diff --git a/docs/docs/get-started/realtime-service.md b/docs/docs/get-started/realtime-service.md index 68f01a1..8972e48 100644 --- a/docs/docs/get-started/realtime-service.md +++ b/docs/docs/get-started/realtime-service.md @@ -9,6 +9,7 @@ TSRPC 的二进制序列化特性,能显著减小包体,帮助实时服务 你可以通过 `npx create-tsrpc-app@latest` 快速创建一个 WebSocket 实时聊天室项目。 ## 实时 API + TSRPC 本身的设计架构是协议无关的,这意味着在[上一节](the-first-api.md)中实现的 API 可以无缝运行在 WebSocket 协议之上。 只需要将 `HttpServer` 替换为 `WebSocketServer`,将 `HttpClient` 替换为 `WebSocketClient` 即可。例如: @@ -16,19 +17,19 @@ import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; - +defaultValue="server" +values={[ +{label: 'Server', value: 'server'}, +{label: 'Client', value: 'client'} +]}> + ```ts -import { WsServer } from 'tsrpc'; +import { WsServer } from 'tsrpc' const server = new WsServer(serviceProto, { - port: 3000 -}); + port: 3000, +}) ``` @@ -36,16 +37,16 @@ const server = new WsServer(serviceProto, { ```ts -import { WsClient } from 'tsrpc-browser'; +import { WsClient } from 'tsrpc-browser' const client = new WsClient(serviceProto, { - server: 'ws://127.0.0.1:3000', - logger: console -}); + server: 'ws://127.0.0.1:3000', + logger: console, +}) -let ret = await client.callApi('Hello', { - name: 'World' -}); +let ret = await client.callApi('Hello', { + name: 'World', +}) ``` @@ -68,12 +69,13 @@ let ret = await client.callApi('Hello', { ```ts title="MsgChat.ts" export interface MsgChat { - name: string, + name: string content: string } ``` 跟 API 协议一样,新增或修改消息定义后,也应该重新生成 ServiceProto,然后同步到前端项目。 + ```shell cd backend npm run proto @@ -85,21 +87,24 @@ npm run sync 消息可以双向传递,即可以从 Server 发给 Client,也可以从 Client 发给 Server。 #### Client 发送 + ```ts client.sendMsg('Chat', { name: 'k8w', - content: 'I love TSRPC.' + content: 'I love TSRPC.', }) ``` #### Server 发送 + Server 同时可能连接着多个 Client,活跃中的所有连接都在 `server.conns`。 要给其中某个 Client 发送消息,可以使用 `conn.sendMsg` ,例如: + ```ts // 给第一个连接的 Client 发送消息 server.conns[0].sendMsg('Chat', { name: 'System', - content: 'You are the first connection.' + content: 'You are the first connection.', }) ``` @@ -110,10 +115,11 @@ server.conns[0].sendMsg('Chat', { #### Server 广播 要给所有 Client 发送消息,可以使用 `server.broadcastMsg()`,例如: + ```ts server.broadcastMsg('Chat', { name: 'System', - content: 'This is a message to everyone.' + content: 'This is a message to everyone.', }) ``` @@ -124,21 +130,21 @@ server.broadcastMsg('Chat', { 监听 / 解除监听消息在 Server 和 Client 类似,例子如下: - +defaultValue="server" +values={[ +{label: 'Server', value: 'server'}, +{label: 'Client', value: 'client'} +]}> + ```ts // 监听(会返回传入的处理函数) -let handler = server.listenMsg('Chat', call=>{ - server.broadcastMsg('Chat', call.msg); -}); +let handler = server.listenMsg('Chat', (call) => { + server.broadcastMsg('Chat', call.msg) +}) // 取消监听 -server.unlistenMsg('Chat', handler); +server.unlistenMsg('Chat', handler) ``` @@ -147,19 +153,19 @@ server.unlistenMsg('Chat', handler); ```ts // 监听(会返回传入的处理函数) -let handler = client.listenMsg('Chat', msg=>{ - console.log(msg.name, msg.content); -}); +let handler = client.listenMsg('Chat', (msg) => { + console.log(msg.name, msg.content) +}) // 取消监听 -client.unlistenMsg('Chat', handler); +client.unlistenMsg('Chat', handler) ``` 不同之处在于,由于 Server 同时可能连接着多个 Client,所以监听消息时收到的参数为 `call: MsgCall`。 -其中除了消息内容( `call.msg` )外,还包含Client 连接( `call.conn` )等信息。 +其中除了消息内容( `call.msg` )外,还包含 Client 连接( `call.conn` )等信息。 而 Client 由于只存在唯一的连接,故监听消息时,收到的参数即为消息本身:`msg: MsgXXXX`。 @@ -167,4 +173,4 @@ client.unlistenMsg('Chat', handler); 使用 `npx create-tsrpc-app@latest` 创建一个带浏览器前端的 WebSocket 项目,里面已经自带了一个极简的聊天室的例子,你也可以在这里查看: -https://github.com/k8w/tsrpc-examples/tree/main/examples/chatroom \ No newline at end of file +https://github.com/k8w/tsrpc-examples/tree/main/examples/chatroom diff --git a/docusaurus.config.js b/docusaurus.config.js index d94f5b7..e18f3e3 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -1,19 +1,24 @@ /** @type {import('@docusaurus/types').DocusaurusConfig} */ module.exports = { title: 'TSRPC - TypeScript 跨平台 RPC 框架', - tagline: '目前世界上唯一支持 TypeScript 复杂类型\n运行时自动检测和二进制序列化的\nTypeScript 开源 RPC 框架', + tagline: + '目前世界上唯一支持 TypeScript 复杂类型\n运行时自动检测和二进制序列化的\nTypeScript 开源 RPC 框架', url: 'https://tsrpc.cn', baseUrl: '/', - onBrokenLinks: 'throw', + onBrokenLinks: 'warn', onBrokenMarkdownLinks: 'warn', favicon: 'img/favicon.ico', organizationName: 'k8w', // Usually your GitHub org/user name. projectName: 'tsrpc', // Usually your repo name. + i18n: { + defaultLocale: 'zh-cn', + locales: ['zh-cn', 'en'], + }, themeConfig: { colorMode: { defaultMode: 'light', disableSwitch: true, - respectPrefersColorScheme: false + respectPrefersColorScheme: false, }, navbar: { title: 'TSRPC', @@ -41,9 +46,13 @@ module.exports = { label: '示例', }, // { to: '/blog', label: 'Blog', position: 'left' }, + { + type: 'localeDropdown', + position: 'right', + }, { label: 'v3.0.4', // by default, show active/latest version label - position: 'right' + position: 'right', }, { href: 'https://github.com/k8w/tsrpc', @@ -113,7 +122,7 @@ module.exports = { // Please change this to your repo. // editUrl: // 'https://github.com/k8w/tsrpc-docs/blob/main/', - routeBasePath: '/' + routeBasePath: '/', }, // blog: { // showReadingTime: true, @@ -130,7 +139,7 @@ module.exports = { plugins: [ function baiduTongJi(context, options) { if (process.env.NODE_ENV !== 'production') { - return {}; + return {} } return { name: 'docusaurus-plugin-baidu-analytics', @@ -148,9 +157,9 @@ module.exports = { })();`, }, ], - }; + } }, - }; - } - ] -}; + } + }, + ], +} diff --git a/i18n/en/docusaurus-plugin-content-docs/current/api/http-client.md b/i18n/en/docusaurus-plugin-content-docs/current/api/http-client.md new file mode 100644 index 0000000..c8ad4a0 --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-docs/current/api/http-client.md @@ -0,0 +1,67 @@ +--- +sidebar_position: 2 +--- + +# HttpClient + +## HttpClientOptions + +Configuration options when creating HttpClient. + +| field | type | default | description | +| :-----------: | :-------: | :-----------------------: | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **server** | `string` | `"http://127.0.0.1:3000"` | Server URL, 以 `http://` 或 `https://` 开头。 | +| **json** | `boolean` | `false` | Enables or disables JSON transfer mode, which will use JSON strings instead of binary serialized transfers. Requires the Server to also enable the jsonEnabled option. | +| **jsonPrune** | `boolean` | `true` | When using JSON transfer mode, whether to automatically exclude extra fields that do not exist in the protocol during the runtime type detection phase. For security reasons, it is always recommended to turn this on. | +| **logger** | `Logger` | `undefined` | Network communication such as API requests/responses will be output to the specified `Logger`. This can be set to `console` if you need to print the log to the console, or to `undefined` if you need to hide the log. | +| **timeout** | `number` | `undefined` | Timeout for API requests in milliseconds, `undefined` or `0` means no time limit. | +| **debugBuf** | `boolean` | `false` | Whether or not to print the binary transfer information in the log, which will make it easier to debug when you develop binary transfer encryption. | + +```ts +export interface HttpOptions { + /** + * Server URL, 以 `http://` 或 `https://` 开头。 + * 默认:"http://127.0.0.1:3000" + */ + server: string + + /** + * 是否启用 JSON 传输模式,启用后将使用 JSON 字符串替代二进制序列化传输。(也进行运行时类型检测) + * 注意:需要 Server 也开启 `jsonEnabled` 选项。 + * 默认:false + */ + json: boolean + + /** + * 使用 JSON 传输模式时,在运行时类型检测阶段,是否自动剔除协议中不存在的额外字段。 + * 出于安全性考虑,建议总是开启,需要动态字段的协议可以通过索引签名定义: + * { + * aaa: string, + * bbb: number, + * // 通过索引签名允许额外字段 + * [key: string]: any + * } + * 默认:true + */ + jsonPrune: boolean + + /** + * API 请求/响应 等网络通讯情况,将被输出至指定的 `Logger` 中。 + * 如果需要将日志打印到控制台,可以设为 `console`;如果需要隐藏日志,可以设为 `undefined`。 + * 默认:`undefined` (这以为着如果你不设置 `logger`,则通讯细节会被隐藏,这有利于防止破解和提升安全性。) + */ + logger?: Logger + + /** + * API 请求的超时时间(毫秒),`undefined` 或 `0` 意味不限时。 + * 默认:`undefined` + */ + timeout?: number + /** + * 是否将二进制传输信息打印在日志中。 + * 当你开发二进制传输加密时,这些信息会便于你调试。 + * 默认:false + */ + debugBuf?: boolean +} +``` diff --git a/i18n/en/docusaurus-plugin-content-docs/current/api/index.md b/i18n/en/docusaurus-plugin-content-docs/current/api/index.md new file mode 100644 index 0000000..5458089 --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-docs/current/api/index.md @@ -0,0 +1,9 @@ +--- +sidebar_position: 1 +--- + +# API + +:::danger WIP +This document is still in preparation ...... Stay tuned. +::: diff --git a/i18n/en/docusaurus-plugin-content-docs/current/api/ws-client.md b/i18n/en/docusaurus-plugin-content-docs/current/api/ws-client.md new file mode 100644 index 0000000..9f53de9 --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-docs/current/api/ws-client.md @@ -0,0 +1,45 @@ +--- +sidebar_position: 3 +--- + +# WsClient + +## WsClientOptions + +创建 `WsClient` 时的配置选项。 + +| 字段名 | 类型 | 默认值 | 描述 | +| :-: | :-: | :-: | - | +| **server** | `string` | `"ws://127.0.0.1:3000"` | Server URL, 以 `ws://` 或 `wss://` 开头。 | +| **logger** | `Logger` | `undefined` | API 请求/响应 等网络通讯情况,将被输出至指定的 `Logger` 中。如果需要将日志打印到控制台,可以设为 `console`;如果需要隐藏日志,可以设为 `undefined`。 | +| **timeout** | `number` | `undefined` | API 请求的超时时间(毫秒),`undefined` 或 `0` 意味不限时。 | +| **debugBuf** | `boolean` | `false` | 是否将二进制传输信息打印在日志中,当你开发二进制传输加密时,这些信息会便于你调试。 | + +```ts +export interface HttpOptions { + /** + * Server URL, 以 `ws://` 或 `wss://` 开头。 + * 默认:"ws://127.0.0.1:3000" + */ + server: string; + + /** + * API 请求/响应 等网络通讯情况,将被输出至指定的 `Logger` 中。 + * 如果需要将日志打印到控制台,可以设为 `console`;如果需要隐藏日志,可以设为 `undefined`。 + * 默认:`undefined` (这以为着如果你不设置 `logger`,则通讯细节会被隐藏,这有利于防止破解和提升安全性。) + */ + logger?: Logger; + + /** + * API 请求的超时时间(毫秒),`undefined` 或 `0` 意味不限时。 + * 默认:`undefined` + */ + timeout?: number; + /** + * 是否将二进制传输信息打印在日志中。 + * 当你开发二进制传输加密时,这些信息会便于你调试。 + * 默认:false + */ + debugBuf?: boolean +} +``` \ No newline at end of file diff --git a/i18n/en/docusaurus-plugin-content-docs/current/cookbook/index.md b/i18n/en/docusaurus-plugin-content-docs/current/cookbook/index.md new file mode 100644 index 0000000..58f1d50 --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-docs/current/cookbook/index.md @@ -0,0 +1,5 @@ +# Cookbook + +:::danger WIP +此文档还在编写中…… 敬请期待。 +::: \ No newline at end of file diff --git a/i18n/en/docusaurus-plugin-content-docs/current/docs/client/_category_.json b/i18n/en/docusaurus-plugin-content-docs/current/docs/client/_category_.json new file mode 100644 index 0000000..370528d --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-docs/current/docs/client/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Client-side development", + "position": 3 +} diff --git a/i18n/en/docusaurus-plugin-content-docs/current/docs/client/supported-platforms.md b/i18n/en/docusaurus-plugin-content-docs/current/docs/client/supported-platforms.md new file mode 100644 index 0000000..2dc0359 --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-docs/current/docs/client/supported-platforms.md @@ -0,0 +1,73 @@ +--- +sidebar_position: 1 +--- + +# List of supported platforms + +## Overview + +The TSRPC client supports many platforms, just bring in `HttpClient` or `WsClient` from different NPM packages as needed, the API is consistent across platforms. + +| Client Platform | NPM Packages | +| :-------------: | :----------: | +| tsrpc-browser | +| tsrpc-miniapp | +| NodeJS | tsrpc | + +For example, for use under the browser project, this corresponds to the `tsrpc-browser` library, which is first installed as follows. + +```shell +npm i tsrpc-browser +``` + +All of the above libraries already come with their own TS type definitions, so just follow the code hints to reference them: ```ts + +```ts +import { HttpClient } from 'tsrpc-browser' +import { WsClient } from 'tsrpc-browser' +import { serviceProto } from '. /shared/protocols/serviceProto' + +const client1 = new HttpClient(serviceProto, { + server: 'https://xxx.com/api', + logger: console, +}) + +const client2 = new WsClient(serviceProto, { + server: 'https://xxx.com/api', + logger: console, +}) +``` + +See the next section [using-client](use-client) for more details on client usage. + +## Using under cross-platform projects + +For example, if you are using a cross-platform front-end framework like [uni-app](https://uniapp.dcloud.io/) or [Taro](https://taro.aotu.io/) to implement an application that needs to support multiple ends at the same time (e.g. browser + WeChat applet + Android App + iOS App). +Then you need to choose the corresponding client library according to the actual running platform. +Since the TSRPC API is renamed between libraries on different platforms, you need to manually `as` an alias when `import`, e.g. + +```ts +import { HttpClient as HttpClientBrowser } from 'tsrpc-browser'; +import { HttpClient as HttpClientMiniapp } from 'tsrpc-miniapp'; +import { serviceProto } from '. /shared/protocols/serviceProto'; + +// Create the corresponding Client according to the actual running platform +export const client = is a WeChat applet ? + // WeChat applets use tsrpc-miniapp + new HttpClientMiniapp(serviceProto, { + server: 'https://xxx.com/api', + logger: console + }) + // Browser and native environments use tsrpc-browser (XMLHttpRequest-compatible environments can use tsrpc-browser) + : new HttpClientBrowser(serviceProto, { + server: 'https://xxx.com/api', + logger: console + }); +``` + +## Using in Cocos Creator + +Currently Cocos Creator 2.x and 3.x versions are perfectly supported. + +Since Cocos Creator is naturally well supported by NPM, you don't need to configure it additionally, just refer to the above example `npm install` and `import`. +If your project is cross-platform, you will also need to create a client based on the platform you are running on, as in the example above. diff --git a/i18n/en/docusaurus-plugin-content-docs/current/docs/client/use-client.md b/i18n/en/docusaurus-plugin-content-docs/current/docs/client/use-client.md new file mode 100644 index 0000000..6c9e79d --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-docs/current/docs/client/use-client.md @@ -0,0 +1,187 @@ +--- +sidebar_position: 2 +--- + +# Using the client + +## Creating a client + +First, create `HttpClient` or `WsClient`, depending on the [supported-platforms] and the server-side protocol. +The client supports concurrent requests, so usually you just need to create and maintain unique instances of the client globally, e.g. + +```ts title="apiClient.ts" +import { HttpClient } from 'tsrpc-browser' +import { serviceProto } from '. /shared/protocols/serviceProto' + +// Create a globally unique apiClient and bring it in from this file if needed +export const apiClient = new HttpClient(serviceProto, { + server: 'https://xxx.com/api', + logger: console, +}) +``` + +The constructors of `HttpClient` and `WsClient` both have 2 arguments. +The first argument, `serviceProto`, is the protocol definition in `serviceProto.ts` generated by our `npm run proto`. +The 2nd parameter is the client configuration, for configurable options see. + +- `HttpClient` configuration options: [HttpClientOptions](/api/http-client#httpclientoptions) +- `WsClient` configuration options: [WsClientOptions](/api/ws-client#wsclientoptions) + +## callApi + +To call the API interface via `client.callApi`. + +```ts +let ret = await client.callApi('interfaceName', { + // request parameters +}) +``` + +:::tip +All methods of TSRPC, including `callApi`, **do not** throw exceptions, so you always **do not need** `catch()` or `try... .catch... ` . +::: + +## ApiReturn + +`callApi` is asynchronous, its return type is `Promise` and contains 2 cases of success and error. + +```ts +export interface ApiReturnSucc { + isSucc: true + res: Res + err?: undefined +} + +export interface ApiReturnError { + isSucc: false + res?: undefined + err: TsrpcError +} + +export type ApiReturn = ApiReturnSucc | ApiReturnError +``` + +## Error handling + +`callApi` is not always successful and some errors may occur. + +### Pitfalls of the traditional approach + +The traditional approach based on `Promise` or callback functions has some pitfalls, which are often the source of bugs, e.g. + +#### i. Handling errors in a decentralized way + +For example, if you use `fetch`, you usually have these errors waiting to be handled. + +```js +fetch(...) + .then(v=>{ + 1. HTTP status code error + 2. HTTP status code is fine, but a business error is returned (e.g. "insufficient balance" "wrong password") + }) + .catch(e=>{ + 1. network error + 2. code error reported + }) +``` + +There are very many potential errors that are scattered all over the place waiting for you to deal with them. Neglecting one can cause problems. + +#### II. Forget to deal with errors + +Many novices don't have the sense to handle errors, for example, in the above example, they may forget to `catch`. +This small oversight can cause big problems, such as a common requirement: "Show Loading during request". + +```js +showLoading(); // Show a full screen Loading +let res = await fetch( ... ); +hideLoading(); // hide Loading +``` + +At first glance it looks fine, but if a network error occurs, `fetch` will throw an exception and the following `hideLoading()` will not be executed. In this way, the loading on the interface will never disappear, often referred to as "stuck". +Don't underestimate it, a good percentage of "stuck" problems in real projects are related to this! + +### Solution for TSRPC + +First look at the example. + +```ts title="frontend/src/index.ts" +let ret = await client.callApi('Hello', { + name: 'World', +}) + +// Handle errors, exception branch +if (!ret.isSucc) { + alert('Error: ' + ret.err.message) + return +} + +// Success +alert('Success: ' + ret.res.reply) +``` + +In TSRPC. + +1. all methods **do not throw exceptions** + - so there is always **no need** for `catch()` or `try.... .catch... `, avoiding the rookie trap. +2. All errors **need to be handled in one place** + - Success is determined by `ret.isSucc`, success is taken as response `ret.res`, failure is taken as error `ret.err` (with error type and detail information). 2. +3. cleverly make you **must do error detection** through the TypeScript type system + - The TypeScript compiler will report an error if you remove the code in the error handling section above, or if you remove `return` after handling the error. + +## Canceling requests + +There are some scenarios where you may need to cancel an API request in the middle of the process. For example, a single page application developed with React, Vue, and you want to cancel an API request that has not been returned after the component `unmount`. + +TSRPC has 3 ways to handle cancellation, and the Promise ** received by the caller of a request after cancellation will neither `resolve` nor `reject` **. + +### Canceling a single request via `SN` + +Each `callApi` generates a unique request number `SN` for the current request, which can be cancelled by `client.abort(SN)`. +The last request number can be obtained by `client.lastSN`, e.g. + +```ts +client.callApi('XXX', { ... }).then(ret => { + // If the request is cancelled, it will not be processed by the client, whether the server returns or not + console.log('Request completed', ret) +}); + +// Log SN immediately after callApi is called +let sn = client.lastSN; + +// after 1 second, if the request still hasn't completed, it will be cancelled +setTimeout(()=>{ + client.abort(sn); +}, 1000) +``` + +### Cancel multiple requests with `abortKey` + +The 3rd parameter that `callApi` can take is an optional configuration item, which contains `abortKey`. +You can specify an `abortKey` in advance at the beginning of the request, and then call `client.abortByKey(key)` to cancel all outstanding requests for the specified `abortKey`. (Completed requests are not affected) + +This is useful for front-end componentized development, e.g. always specify `abortKey: Component ID` when `callApi` inside a component, and then `client.abortByKey(Component ID)` when the component is destroyed, to enable automatic cancellation of internal unfinished requests when the component is destroyed, e.g. + +```ts +// The same abortKey can affect multiple requests +client.callApi('XXX', { ... }, { abortKey: 'MyAbortKey' }).then(ret => { ... }); +client.callApi('XXX', { ... }, { abortKey: 'MyAbortKey' }).then(ret => { ... }); +client.callApi('XXX', { ... }, { abortKey: 'MyAbortKey' }).then(ret => { ... }); + +// After 1 second, cancel those requests that have not yet completed +setTimeout(() => { + client.abortByKey('MyAbortKey'); +}, 1000) +``` + +### Cancel all outstanding requests + +Cancel all outstanding requests under the client with `abortAll()`. + +```ts +client.abortAll() +``` + +## Customizing workflows + +You can customize the client with [Flow](... /flow/flow) to customize client-side workflows, implementing things like [Session and Cookie](. /flow/session-and-cookie), [client-side permission authentication](... /flow/user-authentication), [Mock](... /flow/mock), etc. /flow/mock) and other features. diff --git a/i18n/en/docusaurus-plugin-content-docs/current/docs/client/websocket.md b/i18n/en/docusaurus-plugin-content-docs/current/docs/client/websocket.md new file mode 100644 index 0000000..f4dab7d --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-docs/current/docs/client/websocket.md @@ -0,0 +1,100 @@ +--- +sidebar_position: 3 +--- + +# WebSocket + +At the `callApi` level, `WsClient` is exactly the same as `HttpClient`. +But in addition to that `WsClient` has some other operations. + +## Connections + +If you are creating a WebSocket client `WsClient`, then before `callApi` and `sendMsg`, you need to connect to the server manually: ` + +```ts +let result = await client.connect() + +// The connection may not be successful (e.g. network errors), so remember to handle errors +if (!result.isSucc) { + console.log(result.errMsg) +} +``` + +Correspondingly, the disconnection is. + +```ts +client.disconnect() +``` + +## Disconnect and reconnect + +Small network conditions can always happen from time to time. To make your application more robust, you can implement a disconnect reconnection mechanism via [Flow](... /flow/flow) to implement a disconnect and reconnect mechanism. + +On the client side, `postDisconnectFlow` is triggered after changing from connected to disconnected state (WebSocket only) and is of type + +```ts +Flow<{ + /* The reason for closure passed at `conn.close(reason)` on the Server side */ + reason?: string, + /** + * Whether the connection was manually disconnected by `client.disconnect()`. + * If `false`, the connection was not initiated by the client (e.g. network error, the server disconnected, etc.). + */ + isManual?: boolean +}>(), +``` + +With `isManual` you can determine if the connection is client-initiated or not, and if not, you can wait a short time and reconnect automatically. Example. + +```ts +client.flows.postDisconnectFlow.push((v) => { + if (!v.isManual) { + // wait 2 seconds to reconnect automatically + setTimeout(async () => { + let res = await client.connect() + // reconnect also error, popup error + if (!res.isSucc) { + alert('Network error, the connection has been disconnected') + } + }, 2000) + } + + return v +}) +``` + +:::tip +`postDisconnectFlow` is triggered only when the client changes from connected to disconnected state. Calling `client.connect()` in a disconnected state returns an error when the connection fails, but does not trigger the Flow, so the necessary error handling mechanism after `client.connect()` is also necessary. +::: + +## sendMsg + +Sending real-time messages to the backend via `sendMsg` is common for WebSocket clients. +Due to the cross-transport protocol implementation, `sendMsg` can also be called under `HttpClient`, but since it is short-connected, it can only be sent unidirectionally to the server and cannot receive messages pushed by the server. + +```ts +client.sendMsg('Chat', { + name: 'k8w', + content: 'I love TSRPC.', +}) +``` + +## listenMsg + +Listen to and process live messages from the server with `listenMsg`. + +:::tip +Only works for WebSocket. +::: + +```ts +// Listening (will return the handler function passed in) +let handler = client.listenMsg('Chat', (msg) => { + console.log(msg) +}) + +// unlisten +client.unlistenMsg('Chat', handler) +``` + +Client listens for messages because there is only a unique connection, so when listening for messages, the parameter received is the message itself: `msg: MsgXXXX`. diff --git a/i18n/en/docusaurus-plugin-content-docs/current/docs/deployment/_category_.json b/i18n/en/docusaurus-plugin-content-docs/current/docs/deployment/_category_.json new file mode 100644 index 0000000..a44eabe --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-docs/current/docs/deployment/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "[WIP] Deploy", + "position": 7 +} diff --git a/i18n/en/docusaurus-plugin-content-docs/current/docs/deployment/docker.md b/i18n/en/docusaurus-plugin-content-docs/current/docs/deployment/docker.md new file mode 100644 index 0000000..99052e1 --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-docs/current/docs/deployment/docker.md @@ -0,0 +1,11 @@ +--- +sidebar_position: 3 +--- + +# [WIP] Deploying with Docker + +- Dockerfile example + +:::danger WIP +This document is still in progress ...... Stay tuned. +::: diff --git a/i18n/en/docusaurus-plugin-content-docs/current/docs/deployment/graceful-stop.md b/i18n/en/docusaurus-plugin-content-docs/current/docs/deployment/graceful-stop.md new file mode 100644 index 0000000..52a4729 --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-docs/current/docs/deployment/graceful-stop.md @@ -0,0 +1,9 @@ +--- +sidebar_position: 1 +--- + +# [WIP] graceful stop + +:::danger WIP +This document is still in preparation ...... Stay tuned. +::: diff --git a/i18n/en/docusaurus-plugin-content-docs/current/docs/deployment/kubernetes.md b/i18n/en/docusaurus-plugin-content-docs/current/docs/deployment/kubernetes.md new file mode 100644 index 0000000..b3e36fb --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-docs/current/docs/deployment/kubernetes.md @@ -0,0 +1,12 @@ +--- +sidebar_position: 4 +--- + +# [WIP] Deploying with Kubernetes + +- Health Check +- Bluegreen Release + +:::danger WIP +This document is still in progress ...... Stay tuned. +::: diff --git a/i18n/en/docusaurus-plugin-content-docs/current/docs/deployment/pm2.md b/i18n/en/docusaurus-plugin-content-docs/current/docs/deployment/pm2.md new file mode 100644 index 0000000..da54368 --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-docs/current/docs/deployment/pm2.md @@ -0,0 +1,9 @@ +--- +sidebar_position: 2 +--- + +# [WIP] Deployment with PM2 + +:::danger WIP +This document is still in progress ...... Stay tuned. +::: diff --git a/i18n/en/docusaurus-plugin-content-docs/current/docs/flow/_category_.json b/i18n/en/docusaurus-plugin-content-docs/current/docs/flow/_category_.json new file mode 100644 index 0000000..1b57039 --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-docs/current/docs/flow/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Custom Workflows", + "position": 4 +} diff --git a/i18n/en/docusaurus-plugin-content-docs/current/docs/flow/assets/after-encrypt.png b/i18n/en/docusaurus-plugin-content-docs/current/docs/flow/assets/after-encrypt.png new file mode 100644 index 0000000..503e337 Binary files /dev/null and b/i18n/en/docusaurus-plugin-content-docs/current/docs/flow/assets/after-encrypt.png differ diff --git a/i18n/en/docusaurus-plugin-content-docs/current/docs/flow/assets/before-encrypt.png b/i18n/en/docusaurus-plugin-content-docs/current/docs/flow/assets/before-encrypt.png new file mode 100644 index 0000000..fb6f98b Binary files /dev/null and b/i18n/en/docusaurus-plugin-content-docs/current/docs/flow/assets/before-encrypt.png differ diff --git a/i18n/en/docusaurus-plugin-content-docs/current/docs/flow/assets/custom-get-res.png b/i18n/en/docusaurus-plugin-content-docs/current/docs/flow/assets/custom-get-res.png new file mode 100644 index 0000000..263ffd3 Binary files /dev/null and b/i18n/en/docusaurus-plugin-content-docs/current/docs/flow/assets/custom-get-res.png differ diff --git a/i18n/en/docusaurus-plugin-content-docs/current/docs/flow/assets/custom-get-test.png b/i18n/en/docusaurus-plugin-content-docs/current/docs/flow/assets/custom-get-test.png new file mode 100644 index 0000000..4806409 Binary files /dev/null and b/i18n/en/docusaurus-plugin-content-docs/current/docs/flow/assets/custom-get-test.png differ diff --git a/i18n/en/docusaurus-plugin-content-docs/current/docs/flow/assets/flow.svg b/i18n/en/docusaurus-plugin-content-docs/current/docs/flow/assets/flow.svg new file mode 100644 index 0000000..75f59b1 --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-docs/current/docs/flow/assets/flow.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/i18n/en/docusaurus-plugin-content-docs/current/docs/flow/assets/server-flows.png b/i18n/en/docusaurus-plugin-content-docs/current/docs/flow/assets/server-flows.png new file mode 100644 index 0000000..dd85108 Binary files /dev/null and b/i18n/en/docusaurus-plugin-content-docs/current/docs/flow/assets/server-flows.png differ diff --git a/i18n/en/docusaurus-plugin-content-docs/current/docs/flow/custom-http-response.md b/i18n/en/docusaurus-plugin-content-docs/current/docs/flow/custom-http-response.md new file mode 100644 index 0000000..a5bef2c --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-docs/current/docs/flow/custom-http-response.md @@ -0,0 +1,91 @@ +--- +sidebar_position: 6 +--- + +# Custom HTTP response + +:::tip +The content of this section applies only to `HttpServer`. +::: + +With TSRPC, although there is a companion client, there is no need to go into the details of building HTTP requests. +But there are times when you may want the backend to support some specific common requests. +For example, server information pages, static file services, or some simple control class GET interface or something like that. + +With `Flow`, you can do that too, or even graft TSRPC onto other frameworks. + +## Implementation + +In the node function of `server.flows.preRecvBufferFlow`, there is a parameter field `conn`. +It is the `Connection` of the actual transport protocol. +Since the TSRPC implementation is cross-transport, the `conn` you get directly is of type `BaseConnection`. + +If you create an `HttpServer`, the corresponding `conn` is actually an `HttpConnection`. +At this point, with `conn.httpReq` and `conn.httpEnd` you can get the `req` and `res` objects of the NodeJS native `http` module, and then you're free to do what you want. + +If you want to return your own response, you can do so with `conn.httpRes`. +But remember to break the Flow and subsequent processes by `return undefined` after that. +This way you don't get to the later parts of binary decoding, API parsing, etc. + +For example + +```ts +// Custom HTTP Reponse +server.flows.preRecvBufferFlow.push((v) => { + let conn = v.conn as HttpConnection + + if (conn.httpReq.method === 'GET') { + conn.httpRes.end('Hello World') + return undefined + } + + return v +}) +``` + +Then open the backend service directly in the browser at `http://localhost:3000` and you can see our custom response + +! [](assets/custom-get-res.png) + +Further, you can implement a simple file service: ! + +```ts +// Custom HTTP Reponse +server.flows.preRecvBufferFlow.push(async (v) => { + let conn = v.conn as HttpConnection + + if (conn.httpReq.method === 'GET') { + // Static file service + if (conn.httpReq.url) { + // detect if the file exists + let resFilePath = path.join('res', conn.httpReq.url) + let isExisted = await fs + .access(resFilePath) + .then(() => true) + .catch(() => false) + if (isExisted) { + // return the contents of the file + let content = await fs.readFile(resFilePath) + conn.httpRes.end(content) + return undefined + } + } + + // Default GET response + conn.httpRes.end('Hello World') + return undefined + } + + return v +}) +``` + +! [](assets/custom-get-test.png) + +## Full example + +See: https://github.com/k8w/tsrpc-examples/tree/main/examples/custom-http-res + +:::tip +TSRPC's API and Message communications are via the `POST` method, so make sure you don't affect them. +::: diff --git a/i18n/en/docusaurus-plugin-content-docs/current/docs/flow/flow.md b/i18n/en/docusaurus-plugin-content-docs/current/docs/flow/flow.md new file mode 100644 index 0000000..4fdef20 --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-docs/current/docs/flow/flow.md @@ -0,0 +1,231 @@ +--- +sidebar_position: 1 +--- + +## Flow + +## What is Flow? + +Any framework that can be adapted to more business scenarios requires good extensibility. +`Flow` is a new concept designed by TSRPC for this purpose. + +Similar to pipelines and middleware, `Flow` consists of a set of functions with the same type of input and output, either synchronous or asynchronous. +We call each of these functions a `FlowNode`. +A slight difference from a pipeline is that `FlowNode` can return `null | undefined` to represent a **broken flow**. + +! [](assets/flow.svg) + +Each `Flow` sum has a fixed data type ``, the input and output types of its node functions, defined as follows. + +```ts title="FlowNode Definition" +export type FlowNodeReturn = T | null | undefined +export type FlowNode = ( + item: T +) => FlowNodeReturn | Promise> +``` + +`Flow` is like an array of `FlowNode` that you can append a new node to with `flow.push(flowNode)`. +When `Flow` is executed, it starts with the first `FlowNode` (with the original input parameters), and then takes the output of the previous `FlowNode` as input to the next `FlowNode`, one by one, until it gets the last output or receives `null | undefined` and breaks early. + +Next, let's look at how `Flow` is used in TSRPC. + +## TSRPC Workflow + +TSRPC establishes a unified workflow for the entire communication process. +On top of that, some nodes in the workflow are exposed for customization via `Flow`. `Flow` is common to both the server and the client, and you can use the same programming paradigm to extend the behavior on each side. +For example, on the server side, you can control any of the Flow's in the diagram below to implement custom processes such as transport encryption, authentication, and so on. + +! [](assets/server-flows.png) + +To control the workflow, just `push` your own `FlowNode` functions to these `Flow`s, e.g. one that implements simple login authentication. + +```ts +server.flows.preApiCallFlow.push((call) => { + if (isLogined(call.req.token)) { + // Assuming you have an isLogined method to check if the login token is legitimate + return call // returns normally, which means the process continues + } else { + call.error('You are not logged in') + return null // returns null or undefined, which means the process is broken + } +}) +``` + +### Pre Flow and Post Flow + +TSRPC's built-in `Flow`s are divided into two categories, `Pre Flow` and `Post Flow`, based on their name prefixes. When their `FlowNode` returns `null | undefined` in the middle, both will break the execution of the `Flow` successor node. However, the effect on TSRPC workflows is different. + +- All interruptions of `Pre Flow` **will** interrupt subsequent TSRPC workflows, e.g. an interruption of Client `preCallApiFlow` will prevent `callApi`. +- All interrupts of `Post Flow` **will not** interrupt subsequent TSRPC workflows, e.g. a Server `postConnectFlow` interrupt **will not** prevent connection establishment and subsequent message reception. + +### Server-side Flows + +Obtained via `server.flows`, e.g. +``ts +server.flows.preApiCallFlow.push(call=>{ +// ... +}) + +```` + +| name | role | +| - | - | +| postConnectFlow | after a client connects +| postDisconnectFlow | after the client disconnects | +| preRecvBufferFlow | before processing the received binary data | +| preSendBufferFlow | before sending binary data | +| preApiCallFlow | before executing the API interface implementation +| preApiReturnFlow | before the API interface returns results (`call.succ`, `call.error`) | +| postApiReturnFlow | after the API interface returns results (`call.succ`, `call.error`) | +| postApiCallFlow | after executing the API interface implementation | +| preMsgCallFlow | before triggering the Message listener event | +| postMsgCallFlow | after triggering the Message listener event | +| preSendMsgFlow |before sending a Message +| postSendMsgFlow | after sending a Message + +### Client Flows + +Get through `client.flows`, e.g. +```ts +client.flows.preCallApiFlow.push(v=>{ + // ... +}) +```` + +| name | role | +| ------------------ | ------------------------------------------------------ | ----- | ----------------- | ------------------------------------------------------ | +| preCallApiFlow | before executing `callApi` | +| preApiReturnFlow | before returning the result of `callApi` to the caller | - - - | postApiReturnFlow | before returning the result of `callApi` to the caller | +| postApiReturnFlow | after returning the results of `callApi` to the caller | +| preSendMsgFlow | before executing `sendMsg` | +| postSendMsgFlow | after executing `sendMsg` | +| preSendBufferFlow | before sending any binary data to the server | +| preRecvBufferFlow | before processing any binary data from the server | +| preConnectFlow | before connecting to the server (WebSocket only) | +| postConnectFlow | after connecting to the server (WebSocket only) | +| postDisconnectFlow | after disconnecting from the server (WebSocket only) | + +## Type extensions + +As you can see above, many `Flow`s are accompanied by a pass of `Connection` or `Call`. +In the process, we may want to add some additional data to them. +For example. + +- Add a `call.currentUser` to `call` to pass back the user information parsed from the login state +- Add a `conn.connectedTime` to `conn` to record the connection establishment time + +TSRPC itself does not contain these fields, and using them directly will result in errors, so you need to extend the existing types of TSRPC first. +TSRPC supports type extensions in the following way. + +### Extending the `tsrpc` library types directly + +Extend existing types directly in the ``declare module 'tsrpc'`. + +```ts +declare module 'tsrpc' { + export interface BaseConnection { + // New custom field + connectedTime: number + } + + export interface ApiCall { + currentUser: { + userId: string + nickname: string + } + } +} +``` + +After that, the above custom fields are already legal when you use these types. +They are type correct wherever they are used, e.g. + +- **in Flow** + +```ts +server.flows.postConnectFlow.push((conn) => { + conn.connectedTime = Date.now() +}) + +server.flows.preApiCallFlow.push((call) => { + call.currentUser = { + userId: 'xxx', + nickname: 'xxx', + } +}) +``` + +- **in the API implementation** + +```ts +export async function ApiXXX(call: ApiCall) { + // call.currentUser becomes a legal field + call.logger.log(call.currentUser.nickname) +} +``` + +But if you want to start two different `Server`s in one application, each of them extending different fields, e.g. + +- `server1` adds only `call.currentUser` +- `server2` only adds `call.loggedUser` + +Then extending the `tsrpc` library types directly may lead to mixups, so you need to extend them by creating new types. + +### Creating a new type + +Create new types of `Connection` and `Call`, compatible with existing type definitions: ```tsrpc` + +```ts +type MyConnection = WebSocketConnection & { + connectedTime: number +} + +type MyCall = ApiCall & { + currentUser: { + userId: string + nickname: string + } +} +``` + +Then, where you need to use it, manually replace it with your own type: the + +- In the API implementation + +```ts +export async function ApiXXX(call: MyCall) { + // call.currentUser becomes a legal field + call.logger.log(call.currentUser.nickname) +} +``` + +- In Flow + +```ts +server.flows.preApiCallFlow.push((conn: MyConnection) => { + conn.connectedTime = Date.now() +}) + +server.flows.preApiCallFlow.push((call: MyCall) => { + call.currentUser = { + userId: 'xxx', + nickname: 'xxx', + } +}) +``` + +## Examples + +With the flexible `Flow`, developers can implement many features, and we have compiled some common scenarios. + +- [Implementing Session and Cookie Features](. /session-and-cookie.md) +- [User login and permission authentication](. /user-authentication.md) +- Binary-based transfer encryption](. /transfer-encryption.md) +- [Front-end Local Mock Testing](. /mock.md) +- Custom HTTP responses for GET interfaces, static pages, etc.](. /custom-http-response.md) + + diff --git a/i18n/en/docusaurus-plugin-content-docs/current/docs/flow/mock.md b/i18n/en/docusaurus-plugin-content-docs/current/docs/flow/mock.md new file mode 100644 index 0000000..30ef427 --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-docs/current/docs/flow/mock.md @@ -0,0 +1,134 @@ +--- +sidebar_position: 5 +--- + +## Client Mock + +## What is Mock + +Many times there is a situation where we are developing a front-end page and some functions need to call the back-end interface in order to test the complete process. +Although the protocol has been defined, but the interface has not yet been developed and can not be used. +At this point, how to complete the front-end does not depend on the back-end independent testing it? + +Of course, you can temporarily comment out the location of the API interface call and replace it with a piece of dummy data. But this approach is invasive and unknowingly mixes a lot of test code in your business logic. This lays a lot of potential potholes for yourself, and may bring problems if you forget to remove the test code. + +> Murphy's Law: What can go wrong must go wrong. + +So, the front-end developed a technology called Mock, which allows you to mock the returned data from the back-end, but is implemented outside of the business logic, in other words **non-intrusive**, and you can enable or disable it with a single switch. + +## Implementation + +Implementing the Mock feature in TSRPC requires a dependency on `client.flows.preCallApiFlow`. +This Flow occurs before the actual API call and has a parameter named `return` for the return type of `callApi` which is `ApiReturn`. If you assign a value to the `return` field within the `Flow` function, which is equivalent to returning the result in advance, the client will actually **not** call the back-end interface, but will return that value directly, in exactly the same flow as a real network request. Example. + +#### returns a successful response ahead of time + +```ts +client.flows.preCallApiFlow.push((v) => { + v.return = { + isSucc: true, + res: { + // ... + }, + } + return v +}) +``` + +#### 10% probability of simulating network errors + +```ts +client.flows.preCallApiFlow.push((v) => { + if (Math.random() < 0.1) { + v.return = { + isSucc: false, + err: new TsrpcError('Simulated network error: ' + v.apiName, { + type: TsrpcError.Type.NetworkError, + }), + } + } + return v +}) +``` + +## Mock API + +For the Mock scenario, we can define an object that implements a test API on the front-end. +Then, using the above, before `callApi`, if a corresponding Mock API is found, it is called directly; otherwise it goes to the real request backend interface. + +Mock back-end, Mock API request. + +```ts +// Mock backend, Mock API request + +import { ApiReturn } from 'tsrpc-browser' +import { ServiceType } from '. /shared/protocols/serviceProto' + +// Temporary data storage +const data: { + content: string + time: Date +}[] = [] + +// { interface name: (req: request) => response } +export const mockApis: { + [K in keyof ServiceType['api']]?: ( + req: ServiceType['api'][K]['req'] + ) => + | ApiReturn + | Promise> +} = { + // simulate the backend interface + AddData: (req) => { + let time = new Date() + data.unshift({ content: req.content, time: time }) + return { + isSucc: true, + res: { time: time }, + } + }, + + // Asynchronous is also possible + GetData: async (req) => { + // Simulate a 500ms delay + await new Promise((rs) => { + setTimeout(rs, 500) + }) + + return { + isSucc: true, + res: { + data: data, + }, + } + }, +} +``` + +:::note +The type definition of `mockApis` in the above example is a bit more complicated, using TypeScript's [Mapped Types](https://www.typescriptlang.org/docs/handbook/2/mapped-types.html). If you are not very familiar with TypeScript, you can just copy it and use it. +::: + +Mock API before the client request, if there is, then directly use. + +```ts +// Client Mock +client.flows.preCallApiFlow.push(async (v) => { + // Mock if there is a corresponding MockAPI, otherwise request the real backend + let mockApi = mockApis[v.apiName] + if (mockApi) { + client.logger?.log('[MockReq]', v.apiName, v.req) + v.return = await mockApi!(v.req as any) + client.logger?.log('[MockRes]', v.apiName, v.return) + } + + return v +}) +``` + +When the tests are done, simply comment out the `Flow` in the Mock section or modify `mockApis` to control the Mock switch. +This ensures that the test code is outside of the business logic and gathered in one place for uniform management. + +## Full example + +See: https://github.com/k8w/tsrpc-examples/tree/main/examples/client-mock diff --git a/i18n/en/docusaurus-plugin-content-docs/current/docs/flow/session-and-cookie.md b/i18n/en/docusaurus-plugin-content-docs/current/docs/flow/session-and-cookie.md new file mode 100644 index 0000000..e49084a --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-docs/current/docs/flow/session-and-cookie.md @@ -0,0 +1,77 @@ +--- +sidebar_position: 2 +--- + +# Implementing Session and Cookies + +## Cookies + +### Concepts + +Cookies are a concept in the HTTP protocol and you can see more information on HTTP cookies at [MDN](https://developer.mozilla.org/docs/Web/HTTP/Cookies). + +Since TSRPC is designed to be cross-protocol, meaning it doesn't necessarily run on top of the HTTP protocol, the TSRPC framework **doesn't use** HTTP cookies, but with Flow, we can easily implement the same feature. This makes it more general -- it works on APPs, WeChat applets, and NodeJS clients. + +### Implementation + +The essence of an HTTP cookie is to pass through a set of data that can be set on the server side or on the client side. Simply put, the server side sends back the cookie data, the client stores it and continues to bring it to the parameters until the next request. + +#### 1. Add a common `__cookie` field to all requests and responses (inherited via base class) + +````ts +export interface BaseRequest { + __cookie?: Cookie; +} + +export interface BaseResponse { + __cookie?: Cookie; +} + +export interface Cookie { + sessionId?: string, + [key: string]: any +} +``` 2. + +#### 2. The client receives the `__cookie` from the server and stores it in `localStorage` + +```ts +client.flows.preApiReturnFlow.push(v => { + if (v.return.isSucc) { + if (v.return.res.__cookie) { + localStorage.setItem(CookieStorageKey, JSON.stringify(v.return.res.__cookie)) + } + } + + return v; +}) +```` + +#### 3. When the client sends a request, it automatically adds the locally stored `__cookie` to the request parameters. + +```ts +client.flows.preCallApiFlow.push((v) => { + let cookieStr = localStorage.getItem(CookieStorageKey) + v.req.__cookie = cookieStr ? JSON.parse(cookieStr) : undefined + return v +}) +``` + +You can also add other logic such as timeout time as needed. + +## Session + +### Concepts + +Session refers to session state management (e.g. user login status, shopping cart, game score or other information that needs to be recorded). + +Like cookies, this data needs to be maintained over multiple requests; the difference is that the client cannot modify it at will (e.g. logged-in user IDs) or it is not visible to the client at all (e.g. some access statistics). + +### Implementation + +1. The server generates a Session ID when a new client accesses it, which is passed through a cookie. +2. The server accesses the Session data based on the Session ID, which can be stored in memory or in a database, depending on the situation. + +## Full example + +https://github.com/k8w/tsrpc-examples/tree/main/examples/session-and-cookie diff --git a/i18n/en/docusaurus-plugin-content-docs/current/docs/flow/transfer-encryption.md b/i18n/en/docusaurus-plugin-content-docs/current/docs/flow/transfer-encryption.md new file mode 100644 index 0000000..1ba1b38 --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-docs/current/docs/flow/transfer-encryption.md @@ -0,0 +1,98 @@ +--- +sidebar_position: 4 +--- + +# Transfer encryption + +## Thinking + +TSRPC transmission is binary, so the encryption and decryption algorithm should be binary-based (`Uint8Array`) as well. +There are many mature algorithms that can be used directly (e.g. `RC4`, `AES`, `RES`, etc.), but you can also implement your own private algorithms. + +On both the server and client side, there are `preSendBufferFlow` and `preRecvBufferFlow` that allow you to process the binary before sending and receiving it. +So you only need to encrypt the Buffer in `preSendBufferFlow` and decrypt the Buffer in `preRecvBufferFlow` to encrypt the transmission. + +You can also share this code in the `shared` directory if the encryption and decryption logic is the same on the front and back ends. + +## Implementation + +### Encryption and decryption algorithm + +Let's implement a simple encryption algorithm. + +- Encrypt: take each byte in the binary stream `+1` +- Decrypt: each byte in the binary stream is `-1` + +:::note +In `Uint8Array`, `0 - 1 === 255` and `255 + 1 === 0`, so the above algorithm works under boundary conditions. +::: + +Since the same encryption and decryption operations are performed on both front and back ends, we share this algorithm in the `shared` directory as follows. + +```ts title="shared/models/EncryptUtil" +export class EncryptUtil { + // encrypt + static encrypt(buf: Uint8Array): Uint8Array { + for (let i = 0; i < buf.length; ++i) { + buf[i] -= 1 + } + return buf + } + + // Decrypt + static decrypt(buf: Uint8Array): Uint8Array { + for (let i = 0; i < buf.length; ++i) { + buf[i] += 1 + } + return buf + } +} +``` + +### Extending workflows with Flow + +Now that the encryption algorithm is implemented, we use Flow to extend the server and client to automatically encrypt and decrypt binary data before sending and receiving it. + +#### server-side + +```ts +// Encrypt before sending +server.flows.preSendBufferFlow.push((v) => { + v.buf = EncryptUtil.encrypt(v.buf) + return v +}) +// Decrypt before receiving +server.flows.preRecvBufferFlow.push((v) => { + v.buf = EncryptUtil.decrypt(v.buf) + return v +}) +``` + +#### client + +```ts +// Encrypt before sending +client.flows.preSendBufferFlow.push((v) => { + v.buf = EncryptUtil.encrypt(v.buf) + return v +}) +// Decrypt before receiving +client.flows.preRecvBufferFlow.push((v) => { + v.buf = EncryptUtil.decrypt(v.buf) + return v +}) +``` + +In this way, the transfer process will be automatically encrypted and decrypted using `EncryptUtil`, and you can see the result when you open your browser. + +**Before encryption:** +! [](assets/before-encrypt.png) + +**After encryption:** ! +! [](assets/after-encrypt.png) + +The encryption algorithm in this example is very simple to implement, you can implement more complex algorithms yourself, return a new `Uint8Array` or even change the length. + +## Full example + +See: https://github.com/k8w/tsrpc-examples/tree/main/examples/transfer-encryption diff --git a/i18n/en/docusaurus-plugin-content-docs/current/docs/flow/user-authentication.md b/i18n/en/docusaurus-plugin-content-docs/current/docs/flow/user-authentication.md new file mode 100644 index 0000000..204b917 --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-docs/current/docs/flow/user-authentication.md @@ -0,0 +1,101 @@ +--- +sidebar_position: 3 +--- + +## Login and permission auto-validation + +## Ideas + +Login and permission validation, the practical application of [Session](session-and-cookie.md) in the previous section, means that a user must be logged in or have a specified role to invoke certain interfaces. + +The implementation idea has the following main steps. + +1. login interface: login state (Token) is created after successful login, the identity credentials of the subsequent interface. +2. the client receives the login state from the server, and each subsequent request is automatically brought to the request parameters (the same as the [cookie](session-and-cookie.md) mechanism). +3. protocol configuration: which protocols require login authentication, which protocols require what roles or permissions. 4. +4. automatically verify the login state before calling the API based on the above configuration and the incoming login state, intercept illegal calls, and return the corresponding error message. + +## Create login state + +As with the Session mechanism, the login state should be maintained by the server and read-only by the client. + +When the user logs in successfully, a temporary Token is created, which can be stored in, for example, Redis or a database, as needed. +Based on this Token, the logged-in user ID can be resolved and the user information can be queried. +The Token has an expiration time and will be automatically renewed for any operation within the expiration date. + +## Protocol Configuration + +As mentioned before, the API protocol is defined in `Ptl{interface name}.ts` and the rule is that there are two types of definitions, `Req{interface name}` and `Res{interface name}`. + +In the protocol definition file, there is an additional definable item: **Protocol Configuration**. + +Simply add to the protocol file. + +```ts +export const conf = { + // custom configuration of the protocol, fill in whatever you want + xxx: 'xxx', +} +``` + +Then `npm run proto` will automatically add the contents of `conf` to the generated ServiceProto, so you can get these configurations on the server and client side. + +For example, if we put information about whether the interface requires login authentication and what permissions are required into the protocol configuration. + +```ts +export interface ReqXXXX { + // ... +} + +export interface ResXXXX { + // ... +} + +export const conf = { + needLogin: true, // whether login is required + needRoles: ['admin', 'business'], // user roles that can be accessed +} +``` + +## Automatic authentication + +With the validation rules and the Session data needed for validation, you can use Flow to automate the login and permission validation of the interface. +You can decide whether you want to authenticate only on the server side, or do dual authentication on the server and client side, depending on your needs. + +**Serverside:** + +```ts +server.flows.preApiCallFlow.push((v) => { + // parse the login state + call.currentUser = await UserUtil.parseSSO(req.__ssoToken) + // Get the protocol configuration + let conf = v.service.conf + // If the protocol is configured to require login, block requests that are not logged in + if (conf?.needLogin && !call.currentUser) { + call.error('You are not logged in', { code: 'NEED_LOGIN' }) + return undefined + } + + return v +}) +``` + +**Client:** + +```ts +client.flows.preCallApiFlow.push((v) => { + // Get the protocol configuration + let conf = client.serviceMap.apiName2Service[v.apiName]!.conf + // If the protocol configuration requires login, block requests that are not logged in + if (conf?.needLogin && !isLogined()) { + window.location = 'Jump to login page' + return undefined + } + + return v +}) +``` + +## Full example + +https://github.com/k8w/tsrpc-examples/tree/main/examples/user-authentication diff --git a/i18n/en/docusaurus-plugin-content-docs/current/docs/full-stack/_category_.json b/i18n/en/docusaurus-plugin-content-docs/current/docs/full-stack/_category_.json new file mode 100644 index 0000000..a1250a5 --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-docs/current/docs/full-stack/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "FullStack Engineering", + "position": 5 +} diff --git a/i18n/en/docusaurus-plugin-content-docs/current/docs/full-stack/assets/log.png b/i18n/en/docusaurus-plugin-content-docs/current/docs/full-stack/assets/log.png new file mode 100644 index 0000000..3c2bd93 Binary files /dev/null and b/i18n/en/docusaurus-plugin-content-docs/current/docs/full-stack/assets/log.png differ diff --git a/i18n/en/docusaurus-plugin-content-docs/current/docs/full-stack/common-questions.md b/i18n/en/docusaurus-plugin-content-docs/current/docs/full-stack/common-questions.md new file mode 100644 index 0000000..02aab2d --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-docs/current/docs/full-stack/common-questions.md @@ -0,0 +1,9 @@ +--- +sidebar_position: 4 +--- + +# [WIP] Frequently Asked Questions + +:::Danger WIP +This document is still in preparation ...... Stay tuned. +::: diff --git a/i18n/en/docusaurus-plugin-content-docs/current/docs/full-stack/data-driven-dev.md b/i18n/en/docusaurus-plugin-content-docs/current/docs/full-stack/data-driven-dev.md new file mode 100644 index 0000000..c3ddf8a --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-docs/current/docs/full-stack/data-driven-dev.md @@ -0,0 +1,9 @@ +--- +sidebar_position: 3 +--- + +# [WIP] Data Driven Development + +:::danger WIP +This document is still in progress ...... Stay tuned. +::: diff --git a/i18n/en/docusaurus-plugin-content-docs/current/docs/full-stack/logger.md b/i18n/en/docusaurus-plugin-content-docs/current/docs/full-stack/logger.md new file mode 100644 index 0000000..80d9cd4 --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-docs/current/docs/full-stack/logger.md @@ -0,0 +1,171 @@ +--- +sidebar_position: 1 +--- + +## Logger log management + +## Overview + +A well-engineered application, whether front-end or back-end, must have logs. +Although you can use `console.log` to print logs, it can be a bit tricky when faced with some special scenarios. +For example. + +**server side** + +- you want the API logs to be laid out in a fixed format (e.g. adding IP addresses, logged-in user IDs as prefixes) +- Output logs to a file, or even to a different format (e.g. JSON) +- Report logs to a remote log collection service (e.g. LogTail, LogStash) + +**Client** + +- Report and count exception logs +- Hide logs (to prevent cracking), but show them to developers (for easy debugging) + +To facilitate the extension of the logging process, TSRPC abstracts a unified log management type: `Logger`. + +## Logger + +`Logger` is an abstract log exporter defined in TSRPC's public dependency library `tsrpc-proto`. + +``ts +export interface Logger { +debug(... .args: any[]): void; +log(... . args: any[]): void; +warn(... . args: any[]): void; +error(... .args: any[]): void; +} + +```` + +As with `console`, we define 4 logging levels and their output methods: `debug`, `log`, `warn`, `error`. +Obviously, `console` is a legitimate `Logger`. + +All of TSRPC's Servers and Clients are initialized with a `logger` parameter, which TSRPC uses to output all internal logs. +You can modify the `logger` configuration at initialization to customize the logging output process. +`Server` defaults to `TerminalColorLogger` (which outputs logs with color and time to the console), and `Client` defaults to `undefined` (which outputs no logs). + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + + + + + +```ts +let server = new HttpServer(serviceProto, { + logger: myLogger +}) +``` + + + + + +```ts +let client = new HttpClient(serviceProto, { + logger: myLogger +}) +``` + + + + +## Server-side logging + +On the server side, you should try to avoid using `console` directly, and instead use the `logger` of each of `Server`, `Connection`, and `Call`, e.g. + +```ts +server.logger.log('This is server-level log') +conn.logger.log('This is connection-level log') +call.logger.log('This is call-level log') // call is ApiCall or MsgCall +``` + +where `conn.logger` adds an additional prefix to `server.logger`, such as the IP address. +`call.logger` will add additional prefixes to `conn.logger`, such as the API call path. + +! [](assets/log.png) + +Adding prefixes based on the parent `Logger` is done through `tsrpc`'s built-in `PrefixLogger`. +You can also add or modify these prefixes yourself, for example by adding the user ID to the prefix of `call.logger`. + +```ts +call.logger.prefixs.push('UserID=XXXX') +``` + +:::note +TSRPC Server is designed as a three-tier `Server` -> `Connection` -> `Call` structure, with a top-down one-to-many relationship. +(The exception is HTTP short connections, where 1 `Connection` corresponds to only 1 `Call`) +::: + +Even if there is some public code that does not appear directly in the API implementation code, but may be called indirectly by the API, it is recommended that you pass `Logger` as a parameter. +For example, if you have a public payment method `PayUtil.pay`, there are many APIs that will call it to make a payment, and each payment will leave a log entry. +When there is a billing problem and you consult these logs, you will want to know exactly which API initiated these payment requests. +At this point, passing in `logger` from `ApiCall` can be very useful. + +```ts +export class PayUtil { + static pay( + username: string, + amount: number, + productId: string, + logger?: Logger + ) { + logger?.log(`${username} paid ${amount} for product ${productId}`) + } +} +``` + +:::note +This doesn't affect its compatibility with other projects, after all you can always pass `console` as a legitimate `Logger`. +::: + +## Example + +If we want to report exception logs while hiding DEBUG level logs, for example, we can create a custom `Logger` that + +``ts +let logger: Logger = { +debug(... .args: any[]): ()=>{ +// does nothing, which is equivalent to hiding the log +}; +log(... .args: any[]): ()=>{ +// Let the log still output to the console +console.log(... .args); +// LOG level logs will not be reported +}); +warn(... .args: any[]): ()=>{ +// Let the logs still be output to the console +console.warn(... .args); +// WARN logging, reporting +report log method(... .args); +}; +error(... .args: any[]): ()=>{ +// Make the log still output to the console +console.error(... .args); +// ERROR log, report +report log method(... .args); +}; +} + +```` + +Another example is when we are on the client side and want to hide the log from normal users but make it visible to developers: the + +```ts +// Set a hidden trigger (e.g. with the help of localStorage) +let logger = localStorage.getItem('debug') === 'yes' ? console : undefined + +let client = new HttpClient(serviceProto, { + server: 'http://127.0.0.1', + logger: logger, +}) +``` + +:::tip +The client defaults to `logger: undefined`, where all client logs will be hidden, which helps to raise the crack threshold. +::: diff --git a/i18n/en/docusaurus-plugin-content-docs/current/docs/full-stack/share-codes.md b/i18n/en/docusaurus-plugin-content-docs/current/docs/full-stack/share-codes.md new file mode 100644 index 0000000..bd4a45c --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-docs/current/docs/full-stack/share-codes.md @@ -0,0 +1,9 @@ +--- +sidebar_position: 2 +--- + +# [WIP] Shared Code + +:::danger WIP +This document is still in preparation ...... Stay tuned. +::: diff --git a/i18n/en/docusaurus-plugin-content-docs/current/docs/full-stack/tsrpc-cli.md b/i18n/en/docusaurus-plugin-content-docs/current/docs/full-stack/tsrpc-cli.md new file mode 100644 index 0000000..404a699 --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-docs/current/docs/full-stack/tsrpc-cli.md @@ -0,0 +1,59 @@ +--- +sidebar_position: 1.1 +--- + +# tsrpc-cli command line tools + +``` +Usage notes. + + tsrpc proto Generate TSRPC Proto file + -i, --input Path to the protocol folder used to generate the Proto + -o, --output The path of the output file, not specified will be output directly to the command line + -o XXX.ts and -o XXX.json will output two different formats + -c, --compatible compatibility mode: path to the old Proto file to be compatible with (same as output by default) + --new is not compatible with the old version and generates a new Proto file + --ugly output is a less readable but smaller compressed format + --verbose shows debugging information + --ignore files to ignore from the --input scope + + tsrpc api Automatically generate TSRPC API implementation + -i, --input Path to the Proto file + -o, --output Path to the output API folder + + tsrpc sync --from --to Sync the contents of the folder to the target location in a read-only manner + + tsrpc link --from --to Create a Symlink to the source at the target location for automatic synchronization + + tsrpc build Build the TSRPC backend project + --proto proto file address, default is src/shared/protocols/serviceProto.ts + --proto-dir protocols directory, defaults to the serviceProto.ts directory + + tsrpc encode [exp] Encode JS expressions + [exp] the value to encode (JS expression, e.g. "123" "new Uint8Array([1,2,3])") + -p, --proto Encode the Proto file to be used + -s, --schema encode the SchemaID to be used + --i, --input input as file, not to be used with [exp] (file content as JS expression) + -o, --output The path to the output file, not specified will be output directly to the command line + --verbose show debugging information + + tsrpc decode [binstr] Decode binary data + [binstr] String representation of the binary data to be decoded, e.g. "0F A2 E3 F2 D9" + -p, --proto Decode the Proto file to be used + -s, --schema The SchemaID to be used for decoding + -i, --input input as file, not to be used with [binstr] + --o, --output Output file path, not specified will be output to command line + --verbose Show debugging information + + tsrpc validate [exp] Validate JSON data + [exp] The value to validate (JS expression, e.g. "123" "new Uint8Array([1,2,3])") + -p, --proto Validate the Proto file to be used + -s, --schema Validate the SchemaID to be used + -i, --input input as file, not to be used with [exp] (file content is a JS expression) + + tsrpc show Print the contents of the binary file +``` + +:::danger WIP +This document is still in preparation ...... Stay tuned. +::: diff --git a/i18n/en/docusaurus-plugin-content-docs/current/docs/get-started/_category_.json b/i18n/en/docusaurus-plugin-content-docs/current/docs/get-started/_category_.json new file mode 100644 index 0000000..bd077a6 --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-docs/current/docs/get-started/_category_.json @@ -0,0 +1,5 @@ +{ + "label": "Quick start", + "position": 2, + "collapsed": false +} diff --git a/i18n/en/docusaurus-plugin-content-docs/current/docs/get-started/assets/code-hint.gif b/i18n/en/docusaurus-plugin-content-docs/current/docs/get-started/assets/code-hint.gif new file mode 100644 index 0000000..cd95e3b Binary files /dev/null and b/i18n/en/docusaurus-plugin-content-docs/current/docs/get-started/assets/code-hint.gif differ diff --git a/i18n/en/docusaurus-plugin-content-docs/current/docs/get-started/assets/create-tsrpc-app.gif b/i18n/en/docusaurus-plugin-content-docs/current/docs/get-started/assets/create-tsrpc-app.gif new file mode 100644 index 0000000..ffe2db0 Binary files /dev/null and b/i18n/en/docusaurus-plugin-content-docs/current/docs/get-started/assets/create-tsrpc-app.gif differ diff --git a/i18n/en/docusaurus-plugin-content-docs/current/docs/get-started/assets/type-check.gif b/i18n/en/docusaurus-plugin-content-docs/current/docs/get-started/assets/type-check.gif new file mode 100644 index 0000000..1e8da7f Binary files /dev/null and b/i18n/en/docusaurus-plugin-content-docs/current/docs/get-started/assets/type-check.gif differ diff --git a/i18n/en/docusaurus-plugin-content-docs/current/docs/get-started/create-tsrpc-app.md b/i18n/en/docusaurus-plugin-content-docs/current/docs/get-started/create-tsrpc-app.md new file mode 100644 index 0000000..e167642 --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-docs/current/docs/get-started/create-tsrpc-app.md @@ -0,0 +1,81 @@ +--- +sidebar_position: 1 +--- + +## Create TSRPC application + +## Create a project + +Use the `create-tsrpc-app` tool to quickly create a TSRPC project. + +```shell +npx create-tsrpc-app@latest +``` + +The creation process is interactive, and TSRPC full-stack application projects containing front and back ends can be easily created by selecting the appropriate configuration on the menu. + +! [](assets/create-tsrpc-app.gif) + +:::note +Requires NodeJS 12+, see more help via `npx create-tsrpc-app@latest --help`. +::: + +:::tip +Remember not to forget the `@latest` at the end of the command, this allows you to always create from the latest version of the project template. +::: + +## Full-stack project structure + +TSRPC shares common code such as protocol definitions between front- and back-end projects to get better code hints and improve development efficiency. +Typically, server-side projects are named `backend` and client-side projects are named `frontend`, and they both have a shared code directory `src/shared`. The shared directory is edited on the backend and then read-only synchronized to the frontend, or you can use Symlink to synchronize it automatically. + +A common directory structure is as follows. + +``` +|- backend --------------------------- backend project + |- src + |- shared -------------------- Shared code between front and backend (sync to frontend) + |- protocols ------------- protocol definitions + |- api ----------------------- API implementation + index.ts + +|- frontend -------------------------- frontend project + |- src + |- shared -------------------- front-end and back-end shared code (read-only) + |- protocols + |- index.ts +``` + +:::tip +`frontend` and `backend` are two completely separate projects that can be placed in the same code repository or spread out in two separate code repositories. +::: + +## Local development + +Both frontend and backend projects run local development services in their respective directories via `npm run dev`. + +```shell +cd backend +npm run dev +``` + +```shell +cd frontend +npm run dev +``` + +The project template already comes with a small example, start it and see the effect~ + +## compile build + +Similarly, compile the build with `npm run build` in the respective directory and output it to the `dist` directory by default. + +```shell +cd backend +npm run build +``` + +```shell +cd frontend +npm run build +``` diff --git a/i18n/en/docusaurus-plugin-content-docs/current/docs/get-started/realtime-service.md b/i18n/en/docusaurus-plugin-content-docs/current/docs/get-started/realtime-service.md new file mode 100644 index 0000000..650fdd1 --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-docs/current/docs/get-started/realtime-service.md @@ -0,0 +1,176 @@ +--- +sidebar_position: 3 +--- + +# WebSocket Real-Time Services + +WebSocket Real-Time Service has been one of the major challenges for Web applications. +The binary serialization feature of TSRPC can significantly reduce the package size and help improve the performance of realtime services. +You can quickly create a WebSocket realtime chat room project with `npx create-tsrpc-app@latest`. + +## Real-Time API + +TSRPC itself is designed to be protocol-independent, which means that the API implemented in [the previous section](the-first-api.md) runs seamlessly on top of the WebSocket protocol. +Simply replace `HttpServer` with `WebSocketServer` and `HttpClient` with `WebSocketClient`. Example. + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + + + + +```ts +import { WsServer } from 'tsrpc' + +const server = new WsServer(serviceProto, { + port: 3000, +}) +``` + + + + + +```ts +import { WsClient } from 'tsrpc-browser' + +const client = new WsClient(serviceProto, { + server: 'ws://127.0.0.1:3000', + logger: console, +}) + +let ret = await client.callApi('Hello', { + name: 'World', +}) +``` + + + + +## Real-time messaging + +However, in real-time application scenarios, not all communication is a **request/response** model like `API`. +For example, scenarios such as real-time announcements, chat rooms, etc., expect to receive real-time pushes from the server rather than sending request polls. + +For this reason, TSRPC provides another **publish/subscribe** model: messages (`Message`). + +A message is the smallest unit of end-to-end communication for TSRPC. +We can use TypeScript to define a message type that can be passed **both ways** between the server and the client, and also enjoys automatic type detection and binary serialization features. + +### Defining messages + +Like the API, the definition of a message is stored in the protocols directory `backend/src/shared/protocols` with the file naming convention `Msg{message name}.ts`. +Then declare a type with the same name in it and mark it as `export`, e.g. + +```ts title="MsgChat.ts" +export interface MsgChat { + name: string + content: string +} +``` + +As with the API protocol, after adding or modifying a message definition, the ServiceProto should be regenerated and then synced to the front-end project. + +```shell +cd backend +npm run proto +npm run sync +``` + +### Sending messages + +Messages can be passed in both directions, i.e. from Server to Client and from Client to Server. + +#### Client sends + +```ts +client.sendMsg('Chat', { + name: 'k8w', + content: 'I love TSRPC.', +}) +``` + +#### Server sends + +The Server may be connected to multiple Clients at the same time, and all the active connections are in `server.conns`. +To send a message to one of these Clients, you can use `conn.sendMsg`, e.g. + +```ts +// Send a message to the first connected Client +server.conns[0].sendMsg('Chat', { + name: 'System', + content: 'You are the first connection.', +}) +``` + +:::note +`conn` is short for `Connection`. +::: + +#### Server Broadcast + +To send a message to all Clients, you can use ``server.broadcastMsg()`, for example + +```ts +server.broadcastMsg('Chat', { + name: 'System', + content: 'This is a message to everyone.', +}) +``` + +The advantage of `broadcastMsg()` is that it only performs the serialization process once, reducing CPU overhead compared to going to `sendMsg()` connection by connection. + +### Listening to messages + +Listening / unlistening messages are similar in Server and Client, examples are as follows. + + + + +```ts +// Listening (will return the handler function passed in) +let handler = server.listenMsg('Chat', (call) => { + server.broadcastMsg('Chat', call.msg) +}) + +// unlisten +server.unlistenMsg('Chat', handler) +``` + + + + + +```ts +// Listening (will return the handler function passed in) +let handler = client.listenMsg('Chat', (msg) => { + console.log(msg.name, msg.content) +}) + +// unlisten +client.unlistenMsg('Chat', handler) +``` + + + + +The difference is that since the Server may be connected to multiple Clients at the same time, the parameters received when listening for messages are `call: MsgCall`. +In addition to the message content (`call.msg`), it also contains information about the Client connection (`call.conn`). + +Since the Client only has a unique connection, when listening to the message, the parameter received is the message itself: `msg: MsgXXXX`. + +## Example: Live Chat Room + +Use `npx create-tsrpc-app@latest` to create a WebSocket project with a browser frontend, which already comes with a minimalist chat room example, which you can also see here. + +https://github.com/k8w/tsrpc-examples/tree/main/examples/chatroom diff --git a/i18n/en/docusaurus-plugin-content-docs/current/docs/get-started/the-first-api.md b/i18n/en/docusaurus-plugin-content-docs/current/docs/get-started/the-first-api.md new file mode 100644 index 0000000..b647cc8 --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-docs/current/docs/get-started/the-first-api.md @@ -0,0 +1,319 @@ +--- +sidebar_position: 2 +--- + +# Implementing the first API + +In this section, we will experience a quick implementation of an API service using TSRPC and call it in the browser. + +The full example of this section is available at: https://github.com/k8w/tsrpc-examples/tree/main/examples/first-api + +## Initializing the project + +We start by initializing a Web full-stack project. + +``` +npx create-tsrpc-app@latest first-api --presets browser +``` + +Then we delete the self-contained demo code, i.e. empty the following directories. + +- ` backend/src/shared/protocols` +- `backend/src/api` + +## Concepts + +Before developing API interfaces using TSRPC, it is important to understand a few important concepts. + +- **API** + - An API interface is equivalent to an asynchronous function implemented on the remote end. The input parameters of this function are called **Request** and the return value is called **Response**. +- **Protocol** + - The protocol is the type definition of the API interface, including its request type and response type, and can also contain other configuration information about the interface (e.g. whether login authentication is required, etc.). +- **API Implementation** + - The implementation is the code that implements the functionality of the API interface. +- **Server** + - The implementation side of the API interface, NodeJS 12 or higher. +- **Client** + - The calling end of the API interface, supporting multiple platforms, such as: browser, APP, WeChat applet, NodeJS, etc. + +So to implement a backend API interface, only 3 steps are needed. +**define protocol -> server-side implementation -> client-side invocation**. + +:::note +The reason TSRPC separates the protocol and implementation of the API is that the protocol part can be used by both front and back ends and can be shared across projects, while the implementation obviously exists only on the NodeJS server side. +::: + +## Define the protocol + +### Writing protocol files + +The protocols directory is located in the `backend/src/shared/protocols` directory by default, and protocol files are named `Ptl{interface name}.ts`. + +For example, if we want to implement a protocol named `Hello`, create the file `PtlHello.ts` in that directory and define the request type `ReqHello` and the response type `ResHello` respectively, remembering to export them with the `export` tag. + +```ts +export interface ReqHello { + name: string +} + +export interface ResHello { + reply: string + time: Date +} +``` + +:::tip +TSRPC identifies the protocol (Ptl), request (Req), and response (Res) by name prefix, so be sure to name them as specified. +::: + +### Generate ServiceProto + +[`ServiceProto`](... /server/service-proto.md) is the actual protocol format used by the TSRPC runtime and is automatically generated by executing the following command. + +```shell +cd backend +npm run proto +``` + +:::tip +This command should be run whenever the protocol is modified to regenerate it. +::: + +## Implementing the API + +### Creating implementation files + +TSRPC's API interface implementation is separate from the protocol definition because the protocol definition contains type information that can be shared across projects, while the implementation part can obviously only run on the NodeJS server. +To differentiate, protocol definitions are named `Ptl{interface name}.ts` and implementations are prefixed with `Api{interface name}.ts`. + +The interface implementation is located in `backend/src/api`, which corresponds to the protocol definition, and the prefix of the file name is replaced by `Api` instead of `Ptl`. We've prepared the tools for you to generate it automatically, just follow the previous step with. + +```shell +npm run api +``` + +This way, a blank API file is automatically generated. For the protocol we just defined `PtlHello.ts`, the corresponding generated implementation file is named `ApiHello.ts` and the directory structure is as follows. + +```` +|- backend/src + |- shared/protocols + |- PtlHello.ts definition of the interface Hello + |- api + |- ApiHello.ts implementation of the interface Hello + |- index.ts +index.ts + +:::tip +Existing API files are not overwritten or deleted and can be generated incrementally at any time. +::: + +### Requests and responses +The implementation of the API is an asynchronous function, and input and output to the client is done via the passed-in parameter `call`. +- The request parameter, `ReqHello` as defined in the protocol, is retrieved via `call.req`, where the framework ensures that the type **must be legal**. +- Return the response via `call.succ(res)`, i.e. `ResHello` as defined in the protocol. +- The error is returned via `call.error('readable error message', { xxx: 'xxx' })`, with the second parameter being an optional extra field that you want to return. + +For example. + +```ts title="backend/src/api/ApiHello.ts" +import { ApiCall } from "tsrpc"; + +export async function ApiHello(call: ApiCall) { + if(call.req.name === 'World'){ + call.succ({ + reply: 'Hello, ' + call.req.name, + time: new Date() + }); + } + else{ + call.error('Invalid name'); + } +} +```` + +## Calling the API + +### Shared code + +To invoke the API, the client must have the same protocol definition file, and there may be other common logic code that can be reused on the front and back ends in addition. +For this purpose, we designed the `src/shared` directory. The contents of this directory are always edited in `backend` and then read-only synchronized to `frontend`. + +Execute the following command to complete the synchronization. + +```shell +cd backend +npm run sync +``` + +:::note +The `shared` directory is read-only on the frontend to prevent changes on the frontend from being overwritten by syncing on the backend. If you want to securely modify the contents of the `shared` directory on the frontend as well, you can choose the `Symlink` auto-sync method. +::: + +### Using the client + +Use the TSRPC client to call remote APIs as if they were local asynchronous functions, with full code hinting and type detection. +It supports many platforms, and can be written from different NPM packages `import { HttpClient }` as needed, the rest is the same. + +| client platform | NPM package | +| :-------------: | :---------: | +| tsrpc-browser | +| tsrpc-miniapp | +| NodeJS | tsrpc | + +Since we are creating a browser web project, we are referring to the browser version of the client from `tsrpc-browser`. +For example. + +```ts title="frontend/src/index.ts" +import { HttpClient } from 'tsrpc-browser' +import { serviceProto } from '. /shared/protocols/serviceProto' + +let client = new HttpClient(serviceProto, { + server: 'http://127.0.0.1:3000', + logger: console, +}) +``` + +:::note +Setting `logger: console` prints API calls to the console for easy debugging. +This is because TSRPC transfers are binary serialized, so you will see a mess of code in the network panel of the developer tools. +You can also omit this configuration in a production environment so that no one knows what you are doing 😁. +::: + +### callApi + +The client usage is almost identical across platforms: use `client.callApi()` to call the remote API as if you were calling an asynchronous function locally. +TSRPC is the ultimate experience for front-end access. There are code hints for the entire input and output, no need to even remember the URL, and no need for protocol documentation at all. + +! [code writing experience](assets/code-hint.gif) + +You don't have to worry about low-level errors caused by spelling mistakes, as TSRPC does double type checking at compile time and run time, so you can say goodbye to the painful experience of front- and back-side concatenation. + +! [Code writing experience](assets/type-check.gif) + +:::note +The return type of `callApi` is called `ApiReturn`, so it is often named `ret`. +::: + +### Handling errors and responses + +`callApi` is not always successful and some errors may occur, such as network errors, business errors, etc. +Many inexperienced programmers always fail to remember to handle errors, often leading to many ``stuck'' problems, such as + +```js +showLoading(); +let res = await fetch( ... ); +hideLoading(); +``` + +Forget about `catch` after `fetch`, once an exception is thrown by a network error, `hideLoading` will not be executed and Loading will never disappear and behave as "stuck". + +Solution for #### TSRPC + +1. All methods **do not throw exceptions** + - so there is always **no need** for `catch()` or `try... .catch... `, avoiding the pitfall of novices always forgetting `catch`. 2. +2. all errors are **just handled in one place** + - The success is determined by `ret.isSucc`, and the response `ret.res` is taken for success, and `ret.err` (with error type and details) is taken for failure. +3. cleverly make you **must do error detection** through the TypeScript type system + - The TypeScript compiler will report an error if the code in the error handling section below is removed. + +```ts title="frontend/src/index.ts" +window.onload = async function () { + let ret = await client.callApi('Hello', { + name: 'World', + }) + + // Error + if (!ret.isSucc) { + alert('Error: ' + ret.err.message) + return + } + + // Success + alert('Success: ' + ret.res.reply) +} +``` + +## Test it + +Start the local development server by executing the following commands in the `` frontend`'' and ``backend`'' directories, respectively. + +```shell +npm run dev +``` + +Once the service is up, open http://127.0.0.1:8080 with your browser and see the results + +## Automatic type detection + +TSRPC automatically type detects requests and responses, both at compile time and at run time, on both the client and server side. +So there is no need to care about type safety at all when writing API implementations. + +**Example: request type not legal, error reported at compile time** + +```ts +callApi('Hello', { + name: 12345, // wrong type +}) +``` + +Even if we skip TypeScript's compile-time checks, the TSRPC framework performs checks at runtime. + +- The client performs a checksum first, blocking requests with illegal types locally. +- The server does a second check before executing the API to ensure that the API requests entering the execution phase must be of a legal type. + +**Example: request type is not legal and is blocked by the framework** + +```ts +callApi('Hello', { + name: 12345, +} as any) // as any skips the TypeScript compile-time check + +// request is intercepted, return type error message {isSucc: false, err: ... } +console.log(ret) +``` + +## Binary serialization + +When you open Developer Tools in Chrome and go to the Network panel to grab the packet, you can see that the transfer looks like gibberish because the framework automatically serializes the transfer into a binary code. This is because the framework automatically serializes the transfer into binary encoding. This provides a smaller transfer size and better security than JSON. +Some plaintext is still visible because TSRPC does not encrypt or compress the packet body; developers can do their own encryption and compression of the binary encoding, which we describe in [later section](...). /flow/transfer-encryption.md). + +## Backward Compatible Restful API + +Binary serialization provides better transfer performance, but for compatibility reasons, TSRPC also supports traditional JSON methods such as XMLHttpRequest, fetch, etc. + +The `jsonEnabled` option is enabled on the Server side. + +```ts +const server = new HttpServer(serviceProto, { + ... + // JSON compatible call (POST) + jsonEnabled: true, + ... +}); +``` + +The browser side can be called via JSON with + +```ts +fetch('http://127.0.0.1:3000/Hello', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + name: 'World', + }), +}) +``` + +The call rules are + +- URL is `Service root path/protocol path/interface name` +- Method is `POST` and body is a JSON string +- Header `Content-Type: application/json` needs to be included + +`jsonEnabled` is disabled by default, not recommended for systems with high security requirements (raises the threshold for protocol cracking). + +:::tip +JSON compatible mode, does not affect the automatic type detection work properly, it will not only automatically detect the defined fields, but also will automatically eliminate the undefined redundant fields, to ensure absolute type security. +::: diff --git a/i18n/en/docusaurus-plugin-content-docs/current/docs/get-started/upload.md b/i18n/en/docusaurus-plugin-content-docs/current/docs/get-started/upload.md new file mode 100644 index 0000000..dd56797 --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-docs/current/docs/get-started/upload.md @@ -0,0 +1,82 @@ +--- +sidebar_position: 4 +--- + +# File Uploads + +File uploads are simply too easy for TSRPC! Because the TSRPC protocol itself supports `ArrayBuffer`, `Uint8Array` and other binary types, you can use `ArrayBuffer`, `Uint8Array` and other binary types in your browser. + +Since the TSRPC protocol itself supports binary types such as `ArrayBuffer`, `Uint8Array`, etc., you can use [File API](https://developer.mozilla.org/zh-CN/docs/Web/API/) in your browser FileReader/readAsArrayBuffer) in your browser to read the binary contents of the file and then send it as normal `callApi`. + +Full example: https://github.com/k8w/tsrpc-examples/tree/main/examples/file-upload + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + + + + +``ts +Output interface ReqUpload { +fileName: string, +fileData: Uint8Array +} + +Output interface ResUpload { +url: string; +} + +```` + + + +```ts +Import { ApiCall } from "tsrpc". +import fs from "fs/promises". + +export async function ApiUpload(call: ApiCall) { + // Write the file, aka push the remote file server or whatever. + await fs.writeFile('uploads/' + call.req.fileName, call.req.fileData). + + call.succ({ + url: 'http://127.0.0.1:3000/uploads/' + call.req.fileName + }); +} +```` + + + + + +```ts +async function upload(fileData: Uint8Array, fileName: string) { + // Upload + let ret = await client.callApi('Upload', { + fileData: fileData, + fileName: fileName, + }) + + // Error + if (!ret.isSucc) { + alert(ret.err.message) + return + } + + // Succ + return { + url: ret.res.url, + } +} +``` + + + +``` diff --git a/i18n/en/docusaurus-plugin-content-docs/current/docs/introduction.md b/i18n/en/docusaurus-plugin-content-docs/current/docs/introduction.md new file mode 100644 index 0000000..b1aa3e1 --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-docs/current/docs/introduction.md @@ -0,0 +1,151 @@ +--- +sidebar_position: 1 +--- + +# Introduction + +## What Is TSRPC + +TSRPC is a TypeScript RPC framework for browser Web applications, WebSocket real-time applications, NodeJS microservices, and other scenarios. + + + +Currently, most projects are still using traditional Restful API for frontend and backend communication, which has some pain points. +1. Relying on documentation for protocol definition, frontend and backend linkage is often plagued by low-level errors (such as field case errors, field type errors, etc.). +2. Some frameworks implement the protocol definition specification but require the introduction of [Decorator](https://www.typescriptlang.org/docs/handbook/decorators.html#decorators) or a third-party IDL language. +3. Some frameworks implement type-checking but are unable to support TypeScript's advanced types, such as: +```ts +// userinfo +interface UserInfo { + // source type + from: { type: 'Invitation', fromUserId: string } + | { type: 'Promotional links', url: string } + | { type: 'Direct access' }, + // registeration time + createTime: Date +} +``` +4. JSON supports limited types, for example `ArrayBuffer` is not supported, which makes file uploads implementation very troublesome. +5. The request and response are plaintext, the cracking threshold is too low, and the string encryption is limited and not strong enough. +6. etc. + +We couldn't find an off-the-shelf framework that solved these problems perfectly, so we completely redesigned and created **TSRPC** 。 + +## Overview + +A protocol called `Hello`, from definition and implementation to browser invocation. + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + + + + +```ts +export interface ReqHello { + name: string; +} + +export interface ResHello { + reply: string; +} +``` + + + + + +```ts +import { ApiCall } from "tsrpc"; + +export async function ApiHello(call: ApiCall) { + call.succ({ + reply: 'Hello, ' + call.req.name + }); +} +``` + + + + + +```ts +let ret = await client.callApi('Hello', { + name: 'World' +}); +console.log(ret); // { isSucc: true, res: { reply: 'Hello, World' } } +``` + + + + +## 特性 +TSRPC has some unprecedented and powerful features that give you the ultimate development experience. + +- 🥤 **Pure TypeScript** + - Define protocols directly based on TypeScript `type` and `interface` + - No additional annotations, no Decorator, no third-party IDL language +- 👓 **Automatic type checking** + - Automatic type checking of input and output at compile time and run time + - Always type safe, write business code with confidence +- 💾 **Binary serialization** + - Smaller transfer size than JSON + - More data types than JSON: e.g. `Date`, `ArrayBuffer`, `Uint8Array`, etc. + - Convenient implementation of binary encryption +- 🔥 **The most powerful TypeScript serialization algorithm ever** + - Serialize type definitions in TypeScript source code directly without any annotations + - The first and currently the only binary serialization algorithm that supports TypeScript's advanced types, including + - [Union Type](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#union-types) + - Intersection Type](https://www.typescriptlang.org/docs/handbook/2/objects.html#intersection-types) + - Pick Type](https://www.typescriptlang.org/docs/handbook/utility-types.html#picktype-keys) + - [Partial Type](https://www.typescriptlang.org/docs/handbook/utility-types.html#partialtype) + - [Indexed Access Types](https://www.typescriptlang.org/docs/handbook/2/indexed-access-types.html) + - etc. +- ☎ **Multi-protocol** + - Simultaneous HTTP / WebSocket support +- 💻 **Multi-platform** + - NodeJS / Browser / App / Applet +- ⚡️ **High Performance** + - Single core single process 5000+ QPS throughput (tested on Macbook Air M1, 2020) + - Unit testing, stress testing, DevOps solutions all in place + - Proven in several 10 million user level projects + + +## Compatibility + +Fully compatible with TSRPC on the Server side and with traditional front ends. + +- **Compatible with Restful API calls in JSON form** + - Can use `XMLHttpRequest`, `fetch` or other AJAX frameworks to call the interface as JSON by itself +- **Compatible with pure JavaScript project use** + - You can use TSRPC Client in pure JavaScript projects and enjoy type checking and serialization features +- **Compatible with Internet Explorer 10** + - Browser-side compatibility up to IE 10, Chrome 30 + + + +## Start learning + +While there are many new, exciting and powerful features +But as you can see on [Github](https://github.com/k8w/tsrpc), TSRPC is actually a mature framework that has been open source for over 4 years. Although it hasn't been documented or promoted much, we have used it to develop several projects with millions of DAUs and millions of users, reaching over 100 million+ online users. + +Sorry for the lateness of this document, and hope it will help you in your work. + +[Start learning TSRPC](get-started/create-tsrpc-app.md) \ No newline at end of file diff --git a/i18n/en/docusaurus-plugin-content-docs/current/docs/server/_category_.json b/i18n/en/docusaurus-plugin-content-docs/current/docs/server/_category_.json new file mode 100644 index 0000000..77c6720 --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-docs/current/docs/server/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Server-side development", + "position": 2.1 +} diff --git a/i18n/en/docusaurus-plugin-content-docs/current/docs/server/api-service.md b/i18n/en/docusaurus-plugin-content-docs/current/docs/server/api-service.md new file mode 100644 index 0000000..1c500b8 --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-docs/current/docs/server/api-service.md @@ -0,0 +1,269 @@ +--- +sidebar_position: 3 +--- + +## API Service + +## What is API Service + +An API Service is a service based on the request/response model, where the request parameters are obtained from the client, processed by the server, and a response is returned. +The response is mandatory, whether or not the request is received and processed correctly by the server, the client will receive an explicit reply, both in the form of success and error. +This process is implemented on the server side as an asynchronous function. + +## Implementing an API + +An implementation of an API Service is an asynchronous function, a blank API implementation function, with the following template. + +```ts +export async function ApiXXX(call: ApiCall) {} +``` + +### ApiCall + +The implementation function has one parameter `call: ApiCall`, by which we get the request parameters and return the response. + +#### Print logs + +In an API implementation function, you should use `call.logger` to print the log, not `console`. + +```ts +call.logger.log('xxxxx') +``` + +This is because a `Server` always processes multiple requests in parallel, so the logs of multiple requests are combined together and you can hardly distinguish them. +`call.logger` will automatically add some prefixes to the log messages, such as connection ID, request ID, which makes it easier to filter the logs of the same request when you are debugging. + +! [](assets/log.png) + +If you want to modify these prefixes, for example in a prefix [Flow](. /flow/flow) that resolves the login state, you want to add the logged-in user ID to the prefix of each request log. +This can be modified with `call.logger.prefixes`, e.g. + +```ts +call.logger.prefixs.push('UserID=123456') +``` + +#### gets the request parameters + +`call.req` is the request parameter sent by the client, which corresponds to the protocol type named `Req{interface name}`. +As soon as an `ApiCall` is parsed, the `Server` performs automatic type detection on it. +So whether implementing a function or [Flow](... /flow/flow), `call.req` **must be type-safe**. + +:::note +In fact, since TSRPC is binary serialized (not `JSON.stringify`), the wrong type cannot be transferred at all. +::: + +#### returns a response or error + +The API interface also returns to the client via `call`, in both success and error cases. + +A successful response is returned by `call.succ(res)`, which corresponds to the `Res{interface name}` type defined in the protocol. + +The error response is returned by `call.error(message, data?)`. +The first parameter, `message`, should be a human-readable error message, such as "insufficient balance", "wrong password", etc. +The 2nd parameter, optional, is additional information about the error, and can be passed any field (e.g. error code) that can be fetched on the client side. + +All errors returned to the client are encapsulated in a `TsrpcError` object. + +#### Other + +- The `Connection` that transmitted the request can be retrieved via `call.conn`. +- The `Server` can be retrieved via `call.server`. + +### TsrpcError + +All errors returned to the client by the TSRPC server are encapsulated as `TsrpcError` with the following type definition. +``ts +export class TsrpcError { +// Human-readable error message +message: string; +// Error type +type: TsrpcErrorType; +// Error code +code?: string | int; + + // Any field can be passed in + [key: string]: any; + + // Two constructors, consistent with call.error() + constructor(data: TsrpcErrorData); + constructor(message: string, data?: Partial); + +} + +``` + +It is easy to see that its constructor parameters are the same as `call.error`. This is because `call.error` is actually equivalent to constructing a `TsrpcError` object and returning it to the front-end. + +#### Error Types + +Generally the errors that you return from the active `call.error` in the API are business errors. +But in addition, there are many other errors that the client may encounter during the API call. +For example, network errors, exceptions caused by errors reported by server-side code, exceptions caused by errors reported by client-side code, and so on. +All these errors are included in `TsrpcError`, and we distinguish them by `type`. When you use `call.error`, the error type is set to `TsrpcError.Type.ApiError` by default. + +All error types are defined below, and you can use this enumeration with ``TsrpcError.Type``. + +``ts +export enum TsrpcErrorType { + /** NetworkError */ + NetworkError = "NetworkError", + /* Internal server-side exceptions (e.g. code reporting errors) */ + ServerError = "ServerError", + /* Internal client-side exception (e.g. code error) */ + ClientError = "ClientError", + /* Business error */ + ApiError = "ApiError" +} +``` + +#### error code + +You may also notice that `TsrpcError` has a default error code field `code`, but it is **optional**. + +This is because in real-world projects we find that for the vast majority of projects that don't have multilingual requirements, the error code isn't really useful. +In contrast, a human-readable error message is much more user-friendly, both for developers to debug and for displaying directly in the front-end interface. +That's why TSRPC makes `message` a required field and `code` an optional field that you can use when you have a specific need. + +For example, there is a common error called `you are not logged in`, and wherever the front-end receives this type of error, it should jump to the login screen. +In this scenario, we need to recognize this error of the specified type. +Although you can also identify it by `message`, that is always unreliable and will not work if the error text is changed one day. This can be done with a specific error code `code`, which can be an integer or a string. +We prefer to use strings because it is easier to debug with a clear and easy-to-read error message than reducing the transmission by a few bytes, e.g. + +```ts +call.error('You are not logged in yet', { + code: 'NEED_LOGIN', +}) +``` + +### Organizing the code + +As a project grows in size, it is unlikely that all of the code implementing an API interface will be in one file. +At the same time, we may also have the need to reuse the same piece of business logic code for multiple interfaces. +In short, we need to split the code and call them in the API interface. + +So the question arises, what to do with the hierarchical logging and error responses mentioned above if not within the API implementation function? + +#### 1. Pass `logger` as a parameter + +Passing `logger: Logger` as a parameter to an external public function makes it easy to handle hierarchical logging in the case of multiple API reuse. + +```ts +export class SomeUtil { + static someFunc(logger?: Logger) { + logger?.log('xxxx') + } +} +``` + +:::note +This doesn't affect its compatibility with non-TSRPC projects, after all you can always pass `console` as a legitimate `Logger`. +::: + +#### 2. `throw new TsrpcError()` + +Imagine you're developing a `buy item` API interface and the business process looks like this. + +! [](assets/throw-new-error.svg) + +As you can see, when you split up the business logic and call it through the layers, you end up with an error message to return to the top-level caller. +The actual chain in the business may be longer than that! Typically we might handle this in one of these 2 ways. + +1. cascade the error message back, then do error detection at each call in the API implementation, and `call.error` if a business error is found. + - **Problem:** Very cumbersome and adds significant code; you have to detect errors everywhere, and forgetting one can cause problems. +2. Pass `call` backwards in layers, taking the passed `call` to `call.error` where the actual error occurs. + - **Problem:** Very inelegant and equivalent to coupling pure business logic with the TSRPC framework, which does not facilitate their cross-project use. + +**TSRPC gives a new solution: `throw new TsrpcError()`** + +```ts +import { TsrpcError } from 'tsrpc'; + +export class chargeback module { + chargeback(logger?: Logger){ + if(insufficient balance){ + throw new TsrpcError('Insufficient balance', { + code: 'NOT_ENOUGH_MONEY' + }) + } + } +} +``` + +TSRPC constrains the API interface implementation function to throw an exception if any method is called via `throw`. + +- If the error thrown is `TsrpcError`, it is considered to be an error **that can be returned directly to the client**, and is automatically returned to the client via `call.error`. +- If not, it is considered as an error reported by the server-side code and will return an error of `type` as `ServerError` to the client with the default error message `"Server Internal Error"`. + +Accordingly, because of the exception thrown by `throw`, the API implementation function will also abort execution. +Therefore, `throw new TsrpcError` is an elegant and concise way to return errors directly to the client regardless of the call hierarchy when business code is split outside of the API implementation function. + +### Caution + +`call.succ()` and `call.error()` are two function calls that send return data to the client immediately after execution, but this **is not the same as the end of execution of the **implementation function. +It is fundamentally different from `return` and `throw`. + +For example, this is a `buy item` interface. + +```ts +export async function ApiBuy(call: ApiCall) { + if(insufficient balance){ + call.error('balance is low yo~'); + // return; + } + + ship(); + call.succ({ + result: 'Purchase successful' + }) +} +``` + +Assuming the `insufficient balance` is hit, `call.error` is executed to return an error. +But since there is no `return` after this, the code continues to execute backwards anyway, all the way `shipping()` until `call.succ`. +Although the framework provides protection so that only the first return takes effect, `shipping()` is executed anyway. +Although the balance is insufficient, but shipped, on behalf of the majority of the white whoring party thank you in advance. + +**So, please keep in mind:** + +After `call.error` or `call.succ`, if this is not the last line of code but the process ends here, always remember to `return`. + +## Mounting to Server + +After implementing an API interface, you need to mount it to `Server` in order to provide services to the public. + +### Automounting + +If you created the project using `npx create-tsrpc-app@latest`, this is the default form. +In `backend/src/index.ts` you can see a line of code like this + +```ts +await server.autoImplementApi(path.resolve(__dirname, 'api')) +``` + +`server.autoImplementApi` is to automount the API in the target folder, the rule is + +- Find all `PtlXXX.ts` files according to the directory structure under the protocols directory (`protocols`), and find the corresponding file (`Ptl` prefix to `Api`) under the specified API directory. + - For example `protocols/a/b/c/PtlXXX.ts` corresponds to `api/a/b/c/ApiXXX.ts`. +- Then under that file, look for an export function with the same name as `ApiXXX` and make that function available to the public as an implementation of `PtlXXX`, e.g. + +```ts +export async function ApiXXX(call: ApiXXX) {} +``` + +- If the function of the same name for `ApiXXX` is not found under that file, then `default` is used as the implementation function, e.g. + +```ts +export default async function (call: ApiXXX) {} +``` + +### Manual mounts + +In addition to automounting, you can also mount manually, e.g. + +```ts +server.implementApi('a/b/c/XXX', (call) => { + // API implementation part +}) +``` + +It is recommended that you mount all the API implementation functions before calling ``server.start()` to start the service. diff --git a/i18n/en/docusaurus-plugin-content-docs/current/docs/server/assets/global-collection.gif b/i18n/en/docusaurus-plugin-content-docs/current/docs/server/assets/global-collection.gif new file mode 100644 index 0000000..61ec480 Binary files /dev/null and b/i18n/en/docusaurus-plugin-content-docs/current/docs/server/assets/global-collection.gif differ diff --git a/i18n/en/docusaurus-plugin-content-docs/current/docs/server/assets/log.png b/i18n/en/docusaurus-plugin-content-docs/current/docs/server/assets/log.png new file mode 100644 index 0000000..3c2bd93 Binary files /dev/null and b/i18n/en/docusaurus-plugin-content-docs/current/docs/server/assets/log.png differ diff --git a/i18n/en/docusaurus-plugin-content-docs/current/docs/server/assets/structure.svg b/i18n/en/docusaurus-plugin-content-docs/current/docs/server/assets/structure.svg new file mode 100644 index 0000000..c6fa854 --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-docs/current/docs/server/assets/structure.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/i18n/en/docusaurus-plugin-content-docs/current/docs/server/assets/throw-new-error.svg b/i18n/en/docusaurus-plugin-content-docs/current/docs/server/assets/throw-new-error.svg new file mode 100644 index 0000000..ba41722 --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-docs/current/docs/server/assets/throw-new-error.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/i18n/en/docusaurus-plugin-content-docs/current/docs/server/message-service.md b/i18n/en/docusaurus-plugin-content-docs/current/docs/server/message-service.md new file mode 100644 index 0000000..39f7725 --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-docs/current/docs/server/message-service.md @@ -0,0 +1,211 @@ +--- +sidebar_position: 4 +--- + +# Message Service + +## What is Message Service + +Message Service is a service based on the publish/subscribe model, where messages can be sent in both directions between the server and the client. +It is more commonly used in scenarios such as real-time interaction over long connections (e.g. WebSocket) and server-side pushing. + +:::note +Short connections (e.g. HTTP) can only send messages from the client to the server, but not push messages from the server to the client, so you can use long connections instead if needed. +::: + +Unlike API Service, there is no response to the message. +That is, once a message is sent, it is considered complete and does not ensure that the other party has successfully received and processed the message. +You can also think of a message as the smallest transmission unit of TSRPC, and based on Message Service, you can freely implement various communication models. + +:::tip +Whether a message arrives in an orderly fashion depends on the actual transport protocol. Currently with HTTP and WebSocket, messages arrive in an orderly fashion because the transport layer is based on the TCP protocol. In the future, if the UDP protocol is supported ...... In the future, say ...... +::: + +Using `listenMsg` and `unlistenMsg`, messages can be received and processed as if they were listening for events. + +Like the API interface, messages are transmitted through runtime type detection and binary serialization. +So the message definition is also stored as part of the protocol in [ServiceProto](service-proto#serviceproto), so we need to define it first. + +## Defining messages + +Like the API Service, the definition of a message is identified by **name**. +The naming rules are described in [previous article](service-proto#definition rules), namely + +- The file name is: `Msg{message name}.ts`, and the following items are defined within this file +- Message type name: `Msg{message name}` +- The additional configuration item is: `export const conf = { ... }` + +## Sending messages + +Messages can be sent in both directions between the server and the client. + +### Client-side sending + +Sending from client to server is very simple and requires only. + +```ts +client.sendMsg('Msg name', { + // the message body (i.e. the type definition of MsgXXX) +}) +``` + +### Server-side sending + +Multiple client connections will exist on the server side at the same time, so to send a message, you first have to decide to whom to send it. +A client, i.e. a Connection in TSRPC [three-layer architecture](structure#Protocol-independent three-layer architecture). +Sending a message to a Connection can be implemented in two ways. + +#### sends to the specified client + +First find the Connection you want to send to. + +1. all current client connections are available in `server.conns`. 2. +2. Under each `ApiCall` or `MsgCall` object, you can get the current client connection from `call.conn`. 3. +3. other ways to implement by yourself. + +Then send. + +```ts +conn.sendMsg('Msg name', { + // message body (i.e. type definition of MsgXXX) +}) +``` + +#### Broadcast to multiple clients + +You can also send the same message to multiple clients at the same time: the + +```ts +server.broadcastMsg('message name', { + // message body +}, [conn1, conn2, conn3, ...]) +``` + +The third parameter is the array of target Connections to send to, which is optional and defaults to broadcasting to all active Connections on the current server if not passed. + +The advantage of ``broadcastMsg()` is that it only performs the serialization process once, which reduces CPU overhead. + +## Listening for messages + +At the Server level, you can call `listenMsg` and `unlistenMsg` to receive and process messages. +Since the Server has multiple client connections at the same time, it listens to a `MsgCall` object. + +- `call.msg` is `MsgXXX` +- `call.conn` is the client connection that sent the message +- `call.service` can get the configuration information of this `MsgService` + +```ts +let handler = server.listenMsg('Chat', (call: MsgChat) => { + // call.msg is MsgChat + console.log(call.msg) +}) + +server.unlistenMsg('Chat', handler) +``` + +:::tip +The `listenMsg` method will return the 2nd parameter you passed in. +::: + +## Example + +So for the server, you need to know who you want to send the message to before you send it, so you need to find out the target Connection. There are generally two ways to do this. + +1. filter from `server.conns`. +2. store and maintain the target Connection itself + +Two examples are given to illustrate this. + +### Sending private messages + +If you create a WebSocket project using `npx create-tsrpc-app@latest`, it already comes with a simple chat room. It's a simple implementation that receives a message from the user and broadcasts it to everyone. + +What if we need to add a private messaging feature? For example, if A specifies to send a message to B, only B can receive the message. The idea could be something like this. + +1. add a login process before entering the chat room, where the user logs in and marks the current connection with a `userId`. +2. When sending a private message, look for a connection with `userId` as the target value in `server.conns` and send it + +To do this, you first need to add a `userId` field to the Connection extension. +Typically, giving the base class `BaseConnection` an extension will do the trick. There are [two ways](... /flow/flow#%E7%B1%BB%E5%9E%8B%E6%89%A9%E5%B1%95), we'll start with the simple one. + +```ts +declare module 'tsrpc' { + export interface BaseConnection { + // New custom field + userId: string + } +} +``` + +Then in your login interface, record the `userId` of the current Connection after a successful login. + +```ts +export async function ApiLogin(call: ApiCall){ + if(login successful){ + // Record userId in connection information + call.conn.userId = 'User ID of successful login'; + + call.succ({ + // ... + }) + } +} +``` + +Then, when you need to send a private message, you can. + +```ts +let conn = server.conns.find((v) => v.userId === 'target User ID') +if (conn) { + conn.sendMsg('message name', { + // ... + }) +} +``` + +### Opening a room + +Still with the chat room example above, suppose now there is a need to "open a room", i.e. a user can enter a "room" and the chat messages in the room can only be received by the user who is in the room. + +You can encapsulate the room as a `Room` object as follows. +``ts +export class Room { +static maxRoomId: number = 0; +static rooms: { [roomId: number]: Room } = {}; + + roomId: number; + conns: BaseConnection[] = []; + + constructor(){ + // Generate a unique ID for each new room + this.roomId = ++Room.maxRoomId; + Room.rooms[this.roomId] = this; + } + + join(conn: BaseConnection){ + this.conns.push(conn); + } + + sendRoomMsg(msg: MsgChat){ + server.broadcastMsg('Chat', msg, this.conns); + } + +} + +```` + +When the user creates a room, `new Room()` creates a `Room` object, which generates a unique `roomId`. +You can use the method in the example above to extend a `roomId` field to BaseConnection to mark which room the current connection belongs to. + +When the user sends a message in the room, you can find the corresponding `Room` object based on the `roomId` and call `room.sendRoomMsg` to send the message in the room. + +```ts +server.listen('Chat', (call: MsgChat)=>{ + if(call.conn.roomId){ + let room = Room.rooms[call.conn.roomId]; + room.sendRoomMsg(call.msg); + } +}) +```` + +Compared to the previous method, this approach keeps the Connection in the business logic and avoids having to iterate through `server.conns` each time. diff --git a/i18n/en/docusaurus-plugin-content-docs/current/docs/server/service-proto.md b/i18n/en/docusaurus-plugin-content-docs/current/docs/server/service-proto.md new file mode 100644 index 0000000..17005e1 --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-docs/current/docs/server/service-proto.md @@ -0,0 +1,74 @@ +--- +sidebar_position: 2 +--- + +## Protocol and runtime principles + +## Runtime type detection implementation principles + +It is well known that TypeScript type detection only happens at compile time, because type information (e.g. `type`, `interface`) is erased at compile time. But TSRPC can detect these erased type information at runtime as well? +Besides, the TypeScript compiler is several MB, while TSRPC is only a few tens of KB ...... + +In fact, this is because we follow the TypeScript type system and independently implement a lightweight type system that can do type detection and even binary serialization at runtime. It supports most of the commonly used TypeScript types. [List of supported types](... /tsrpc-inside/supported-types) + +:::note +This is another independent open source project [TSBuffer](https://github.com/k8w/tsbuffer), if you have separate TypeScript data type runtime detection and serialization needs can take a look. The documentation is temporarily vacant, we will improve it in the future, and of course you can contact the living author (see the bottom of the page). +::: + +## Define the protocol + +### ServiceProto + +In summary, TSRPC has its own set of protocol and type definitions at runtime, and this definition format is named `ServiceProto`. +Generally speaking, you don't need to go into the details of this format, as it is defined via the [command line tool](...). /full-stack/tsrpc-cli) automatically. +If you are using a project created with `npx create-tsrpc-app@latest`, the generation command is already built into the `npm run proto` of the backend project. serviceProto is automatically generated to `protocols/serviceProto.ts`. + +### Define rules + +`tsrpc-cli` identifies protocols entirely by **name**, which includes filenames, type names, etc. +So be sure to strictly follow the name prefixes specified by TSRPC, as you did in [previous post](...). /get-started/the-first-api#Writing Protocol Files), the naming rules are summarized below. + +#### API Service + +- The file name is: `Ptl{interface name}.ts` and the following items are defined in this file +- Request type name: `Req{interface name}` (must be `export`) +- The response type is named: `Res{interface name}` (must `export`) +- The additional configuration item is: `export const conf = { ... }` + +#### Message Service + +- The file name is: `Msg{Message name}.ts` and the following items are defined in this file +- Message type name: `Msg{Message name}` +- Additional configuration items are: `export const conf = { ... }` + +The protocol information and additional configuration items can be obtained at runtime via `call.service` or `server.serviceMap`. + +:::tip +Although it is specified that requests and responses must be defined within the `PtlXXX.ts` file, they are free to refer to other files of external types. However, since protocol directories are shared across projects, for convenience, you should ensure that all protocol dependencies are located inside the `shared` directory and are not referenced from `node_modules` unless you are sure that they are installed in both front and back-end projects. +::: + +### Synchronizing protocols + +The ServiceProto and protocol directories are also needed on the client side, so you can sync them to the client project in any way you like. Our project template has the `npm run sync` command built in for you. + +If you don't check Symlink when creating your project, `npm run sync` is implemented to delete the target folder, then copy it from the backend and finally set it to read-only. The point of making it read-only is to prevent front-end changes from being overwritten by new syncs. + +If you want to synchronize changes on both the front and back ends, you can enable Symlink when you create the project, where `npm run sync` just creates a shortcut to the target location. At this point, both sides are actually the same directory, so you can synchronize changes and don't need to run `npm run sync` every time. + +If you're on Windows and you're using Git, Symlink may become a file when you clone a project with Symlink in it locally. +If this happens, you just need to re-run `npm run sync` in the backend directory to generate Symlink. + +## Protocol changes + +Since the TSRPC runtime actually resolves `ServiceProto`, whenever the protocol changes, you should run `npm run proto` to regenerate it and then `npm run sync` to sync them out. + +This brings up an important question: will the regenerated `ServiceProto` remain compatible with the old one? + +Each interface, type definition in TSRPC, has an ID, which is a self-incrementing number. +For example, you have 3 interfaces `PtlA`, `PtlB`, `PtlC`, which might be coded as `1`, `2`, `3`. +But imagine this situation, suppose you delete `PtlB` and add a new `PtlD`, how will the interface IDs be coded for the newly generated protocol? + +Don't worry about this situation! Because when you run `npm run proto`, `tsrpc-cli` will check if there is a file with the same name at the target location, and if there is, it will read it, compare the old and new protocols and make it as compatible as possible to make sure there is no ID conflict. + +Of course, there is still another possibility of conflict, where there is an incompatible change in the type itself. +For example, changing from `a: string` to `a: number`, in which case the old and new protocols are not compatible. When publishing, you can use something like blue-green publishing to prevent protocol conflicts caused by inconsistent versions of the front and back ends. diff --git a/i18n/en/docusaurus-plugin-content-docs/current/docs/server/structure.md b/i18n/en/docusaurus-plugin-content-docs/current/docs/server/structure.md new file mode 100644 index 0000000..0e6928b --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-docs/current/docs/server/structure.md @@ -0,0 +1,52 @@ +--- +sidebar_position: 1 +--- + +## Server-side architecture + +## Protocol-independent three-layer architecture + +TSRPC was designed from the start to be **binary** and **transport protocol independent**, which means you can easily extend it to other transport protocols such as native TCP, UDP, etc. +To achieve this, TSRPC Server is designed as a three-layer architecture: ! + +! [](assets/structure.svg) + +- **Server**: the provider of the `Service + - The base class is `BaseServer`, which derives `HttpServer`, `WsServer`, etc. according to the actual transport protocol. +- **Connection**: transport layer connection between client and server, binary transport channel + - The base class is `BaseConnection`, which derives `HttpConnection`, `WsConection`, etc. according to the actual transport protocol. + - Under a long connection, you can call `sendMsg()` under this object to send a message directly to the client. +- **Call**: A single service call initiated by the client, containing all the information sent by the client + - The base class `ApiCall` represents a call to `ApiService`, through which the request is fetched and the response is returned. Derived from `ApiCallHttp`, `ApiCallWs`, etc. according to the actual transport protocol. + - The base class `MsgCall` represents a call to `MsgService`, through which the Message content is retrieved. Derived from the actual transport protocol are `MsgCallHttp`, `MsgCallWs`, etc. + +Knowing their base classes helps you to implement [type extensions](... /flow/flow#type extensions), and all the types you extend to the base class will take effect for the subclasses as well. + +Usually, we want to implement API interfaces and other features that are cross-protocol, i.e., run on both the HTTP protocol and the WebSocket protocol. +So instead of `ApiCallHttp` or `ApiCallWs`, the reference in the API implementation is to their protocol-independent base class `ApiCall`. +If you have an interface that only works on the WebSocket platform, then you can instead reference `ApiCallWs` so that you can get some control that is exclusive to WebSocket. Same for `MsgCall`, `Connection`. + +:::tip +A `Connection` can have one or more `Call`s, depending on its transport protocol. For example, HTTP short connections can only carry a single `Call`, while WebSocket long connections can send and receive multiple `Calls` at the same time. +::: + +## Service + +TSRPC can provide services called `Service`, which are of two types. + +- `ApiService`: i.e. API interface service, based on request/response model, can only be requested by the client and responded to by the server. +- `MsgService`: Message service, based on publish/subscribe model, can send messages in both directions between the client and the server. + +:::note +The bidirectional sending of `MsgService` is limited to long connections (e.g. WebSocket). +::: + +:::tip +The difference is that `ApiService` is guaranteed reachable and will return an explicit response or error whether or not the server has received and processed the message correctly. +In the case of `MsgService`, a single Message is sent in one direction and does not need to be returned, so there is no guarantee that the other side has received and processed it correctly (similar to UDP). +::: + +## Summary + +To summarize, TSRPC Server is mainly responsible for the implementation and external provisioning of the service, through a three-layer architecture to achieve a protocol-independent design. +The specific code-level usage will be described in the subsequent articles of this section. diff --git a/i18n/en/docusaurus-plugin-content-docs/current/docs/server/use-database.md b/i18n/en/docusaurus-plugin-content-docs/current/docs/server/use-database.md new file mode 100644 index 0000000..b0a7038 --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-docs/current/docs/server/use-database.md @@ -0,0 +1,300 @@ +--- +sidebar_position: 5 +--- + +## Using the database + +## Choice of database + +There are many databases to choose from, depending on the situation. +However, in most cases, to make the most of the TypeScript type feature, we recommend that you use a database that **supports JSON**. +Why is that? + +Suppose we have a data table for storing articles (Post in English) with the following type definition. + +```ts +export interface Post { + id: number + title: string + content: string + createUserId: number + createTime: Date + // updateUser and updateTime are optional (because they don't exist when first created) + updateUserId?: number + updateTime?: Date +} +``` + +There are 2 fields `updateUserId` and `updateTime`, which are optional. This makes sense, since these two fields are not included when the article is first created. (They are only set when the article is updated) + +But such a type definition actually potentially buries a hole in that it does not constrain that `updateUserId` and `updateTime` must **both** appear or not. If you accidentally set only one of them in your code and forget the other, the type is still legal and TypeScript will not prompt any error. +But it is clearly wrong in business, and this is a bug! + +> Murphy's Law: What can go wrong must go wrong. + +We can totally avoid it at the type definition stage! +This is a typical scenario: **a set of fields either appear at the same time or not **at the same time. +For this scenario, you can wrap the whole set of fields into a subobject and then make the whole optional, e.g. + +```ts +export interface Post { + id: number + title: string + content: string + // To keep the style consistent, create is also handled this way + create: { + uid: number + time: Date + } + update?: { + uid: number + time: Date + } +} +``` + +In this case, `update.uid` and `update.time` either appear at the same time or not at the same time. If only one of them is passed, TypeScript will prompt an error at the compile stage. +Not only do you avoid the aforementioned pitfalls, but you will also find that the code seems to be more elegant: the previously repeated word `update` now appears only 1 time, and there is less code. + +There are many similar examples where you can make clever use of type definitions to make the data structure more rigorous, but this inevitably requires the use of nested JSON structures. +So to make better use of TypeScript's type features, we recommend that you use a database that supports storing JSON structures. + +MongoDB is a good choice. + +:::note +If you are just developing a local single-process lightweight service, [LowDB](https://github.com/typicode/lowdb) is also a good choice. +::: + +## Using MongoDB + +Here we take MongoDB as an example. + +If you are not familiar with Mongo, you can also take a look at the official [Quick Start](https://docs.mongodb.com/manual/tutorial/getting-started/). + +### Installation + +MongoDB already provides an official NodeJS client, see the [official documentation](http://mongodb.github.io/node-mongodb-native/) for details on how to use it. +First install it with. + +```shell +npm i mongodb +``` + +Since we are using TypeScript, we also need its type definition. + +```shell +npm i @types/mongodb --save-dev +``` + +Then it's ready to be used in code. + +### Configuration and startup + +MongoDB's clients are all asynchronous APIs and it automatically maintains a connection pool, so you only need to create a shared `Db` instance globally. +For easy management of global instances, you can wrap them together in a single package, e.g. + +```ts +import { Db, MongoClient } from 'mongodb' + +export class Global { + static db: Db + + static async initDb() { + const uri = + 'mongodb://username:password@xxx.com:27017/test?authSource=admin' + const client = await new MongoClient(uri).connect() + this.db = client.db() + } +} +``` + +:::tip +You can put the connection configuration in a configuration file or in environment variables. +::: + +You need to connect to the database before the service starts, so modify the startup process in ``index.ts`'' as follows + +```ts title="index.ts" +async function main() { + // Auto implement APIs + await server.autoImplementApi(path.resolve(__dirname, 'api')) + + // Connect to the database before starting the service + await Global.initDb() + + await server.start() +} +``` + +Then, you can call MongoDB in your interface using ``Global.db`'', for example + +```ts +export async function ApiGetPost(call: ApiCall) { + let op = await Global.db.collection('Post').findOne({ + _id: new ObjectID(call.req._id), + }) + // ... +} +``` + +:::tip +Usually you don't need to manually go and close the connection, keeping a long connection to the database will make your interface more responsive. +::: + +### Table name and structure mapping + +As you saw in the example above, we can tell TypeScript the table structure type by writing something like `db.collection('table name')`. +But again, you've laid a small hole for yourself. + +> Murphy's Law: What can go wrong must go wrong. + +What if the table name is misspelled? What if the type name is associated with the wrong type? These are common occurrences. + +Again, you can take advantage of TypeScript's type system to get around these problems right from the start. +First, define an `interface` that shows all table names and their types. + +```ts +export interface DbCollectionType { + // table name: type name + Post: DbPost + User: DbUser + Comment: DbComment +} +``` + +Then, implement a `.collection` method on your own to automatically associate table names and type names using TS's generics. + +```ts +import { Collection, Db, MongoClient } from "mongodb"; + +export class Global { + + static db: Db; + static async initDb() { ... } + + static collection(col: T): Collection { + return this.db.collection(col); + } +} +``` + +Now you can use `Global.collection` instead of `Global.db.collection`, with automatic code hinting and type constraints. + +! [](assets/global-collection.gif) + +### Handling ObjectIDs + +MongoDB automatically creates a `_id` field for all records, of type `ObjectID`, which is referenced from the `mongodb` NPM package. +So the `ObjectID` type is not available to the front-end, and needs to be converted to a string when communicating with the front-end: ``ts + +```ts +import { ObjectID } from 'mongodb' + +let _id = new ObjectID('60d9f7d32b285522785b3cb5') +let str = _id.toHexString() +``` + +Therefore, you should not use `ObjectID` for all type definitions and files in the `shared` directory. Because they are to be shared across projects, the front end may not have `mongodb` installed. +But your database table structure definition needs `_id: ObjectID`, and the front-end needs to use the table structure type, so declaring two copies is obviously burying the hole again. (Type redundancy) + +TSRPC provides a tool type `Overwrite` to solve this problem. + +#### 1. Define common types for the front and back ends in the shared directory + +To ensure cross-project availability, `_id` is set to `string`. + +```ts title="shared/protocols/models/Post.ts" +export interface Post { + _id: string + title: string + content: string +} +``` + +#### 2. Rewriting the actual database storage structure on the backend using Overwrite + +To avoid name confusion, we name the database table structure definition for `Post` as `DbPost`. + +```ts title="backend/src/models/dbItems/DbPost.ts +import { ObjectID } from 'mongodb' +import { Overwrite } from 'tsrpc' +import { Post } from '... /... /shared/protocols/models/Post' + +// { _id: ObjectID, title: string, content: string } +export type DbPost = Overwrite< + Post, + { + _id: ObjectID + } +> +``` + +In this way, you can minimize type redundancy and use `Overwrite` to do a small amount of field rewriting for individual scenarios on the back end. + +### Please note the pitfalls + +TypeScript works in strict mode by default, and there is a difference between `null` and `undefined`. + +But MongoDB has a pitfall here, for example if you. + +```ts +db.collection('Test').insertOne({ + value: undefined, +}) +``` + +Or. + +```ts +db.collection('Test').updateOne( + { _id: 'xxx' }, + { + $set: { + value: undefined, + }, + } +) +``` + +The above operation, in MongoDB, will only make `value` `null`, not `undefined`. +The next time you get the data back, you'll find that `value` becomes `null`. + +By default, TSRPC's automatic type detection, like TypeScript's strict mode, distinguishes between `null` and `undefined`, which causes the response to not be returned properly. +There are two solutions. + +#### 1. Avoid the above usage + +TSRPC does not encode undefined, i.e. if you send `{ value: undefined }` from the client, the server receives `{}`. +Remember that setting a field to `undefined` in MongoDB's `update` should be done with `$unset: { field name: 1 }`, not `$set: { field name: undefined }`. + +#### 2. Make TSRPC non-strictly checksum `null` and `undefined` + +That is, make TSRPC treat `null` and `undefined` as identical, consistent with how `strictNullChecks: false` behaves in `tsconfig`. +This is easiest to do as long as your business does not treat `null` and `undefined` strictly differently. + +## Reducing type redundancy + +A common scenario for CRUD interfaces is to allow only a limited number of fields to be sent by the client for data table structures, with the rest of the fields maintained by the client. +Using the TypeScript tool types `Pick`, `Omit`, `Partial`, you can also define them with minimal redundancy, e.g. + +```ts +export interface ReqAddPost { + // Remove the specified 4 fields from the Post + newPost: Omit +} +``` + +```ts +export interface ReqUpdatePost { + // { _id: string, title?: string, content?: string } + update: { _id: string } & Partial> +} +``` + +:::tip +Even if the client sends additional fields outside the protocol, the TSRPC type system will automatically reject them, ensuring strict type and field security. +::: + +## Full example of CRUD + +See: https://github.com/k8w/tsrpc-examples/tree/main/examples/mongodb-crud diff --git a/i18n/en/docusaurus-plugin-content-docs/current/docs/test/_category_.json b/i18n/en/docusaurus-plugin-content-docs/current/docs/test/_category_.json new file mode 100644 index 0000000..a3e483a --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-docs/current/docs/test/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "[WIP] Testing", + "position": 6 +} diff --git a/i18n/en/docusaurus-plugin-content-docs/current/docs/test/stress-testing.md b/i18n/en/docusaurus-plugin-content-docs/current/docs/test/stress-testing.md new file mode 100644 index 0000000..550dfd8 --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-docs/current/docs/test/stress-testing.md @@ -0,0 +1,14 @@ +--- +sidebar_position: 3 +--- + +# [WIP] Stress Testing + +Quickly perform stress tests using TSRPC's already available toolchain. + +- TPS +- QPS + +:::danger WIP +This document is still in progress ...... Stay tuned. +::: diff --git a/i18n/en/docusaurus-plugin-content-docs/current/docs/test/unit-testing.md b/i18n/en/docusaurus-plugin-content-docs/current/docs/test/unit-testing.md new file mode 100644 index 0000000..cdc22de --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-docs/current/docs/test/unit-testing.md @@ -0,0 +1,12 @@ +--- +sidebar_position: 2 +--- + +# [WIP] Unit Testing + +- Using Mocha for API-level unit testing +- Configure Mocha breakpoint debugging in VSCode + +:::danger WIP +This document is still in progress ...... Stay tuned. +::: diff --git a/i18n/en/docusaurus-plugin-content-docs/current/docs/tsrpc-inside/_category_.json b/i18n/en/docusaurus-plugin-content-docs/current/docs/tsrpc-inside/_category_.json new file mode 100644 index 0000000..bdd6980 --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-docs/current/docs/tsrpc-inside/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "TSRPC inner design", + "position": 8 +} diff --git a/i18n/en/docusaurus-plugin-content-docs/current/docs/tsrpc-inside/k8w-extend-native.md b/i18n/en/docusaurus-plugin-content-docs/current/docs/tsrpc-inside/k8w-extend-native.md new file mode 100644 index 0000000..95e034d --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-docs/current/docs/tsrpc-inside/k8w-extend-native.md @@ -0,0 +1,13 @@ +--- +sidebar_position: 4 +--- + +# [WIP] k8w-extend-native + +- k8w-linq-array +- k8w-super-object +- k8w-super-date + +:::danger WIP +This document is still in progress ...... Stay tuned. +::: diff --git a/i18n/en/docusaurus-plugin-content-docs/current/docs/tsrpc-inside/serialization-algorithm.md b/i18n/en/docusaurus-plugin-content-docs/current/docs/tsrpc-inside/serialization-algorithm.md new file mode 100644 index 0000000..ca70fe9 --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-docs/current/docs/tsrpc-inside/serialization-algorithm.md @@ -0,0 +1,12 @@ +--- +sidebar_position: 2 +--- + +# [WIP] Serialization Algorithm + +- How is serialization done by TSRPC? +- TSBuffer Introduction + +:::danger WIP +This document is still in progress ...... Stay tuned. +::: diff --git a/i18n/en/docusaurus-plugin-content-docs/current/docs/tsrpc-inside/supported-types.md b/i18n/en/docusaurus-plugin-content-docs/current/docs/tsrpc-inside/supported-types.md new file mode 100644 index 0000000..cc44333 --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-docs/current/docs/tsrpc-inside/supported-types.md @@ -0,0 +1,312 @@ +--- +sidebar_position: 1 +--- + +# List of supported types + +## Basic types + +### Boolean Type + +```ts +type A = boolean +``` + +### Number Type + +#### Built-in number type + +```ts +type A = number // 64-bit, encoded as double +type B = bigint // encoded as Buffer, arbitrary length +``` + +#### Additional scalar types + +TSRPC also provides some additional numeric types that are equivalent to `number` / `bigint` at compile time. +But at runtime, they are additionally checked for integers and signed, and a more efficient encoding algorithm is used. +Just refer to it from 'tsrpc-proto'. + +```ts +import { int, uint, bigint64, biguint64 } from 'tsrpc-proto' + +type A = int // encoded as Varint (ZigZag) +type B = uint // encoded as Varint +type C = bigint64 // encoded as 64-bit int +type D = biguint64 // encoded as 64-bit uint +``` + +:::note +`tsrpc-proto` is a public dependency for all TSRPC server and client libraries, so no additional installation is needed. +::: + +### String Type + +```ts +type A = string +``` + +:::note +The new [string template](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-3.html#template-string-type-) feature in TypeScript 4.3 will be supported in the future. improvements) feature will be supported in the future. +::: + +### ArrayBuffer Type + +ArrayBuffer and all TypedArray + +```ts +type A = ArrayBuffer +type B = Uint8Array +type C = Int32Array +// all TypedArray, not to be enumerated +``` + +### Enum Type + +```ts +enum MyEnum { + Option1, + Option2, + Option3 = 'XXXXXX', +} +type A = MyEnum +``` + +:::tip +You can safely set strings to enum values. Because of some nifty trick, no matter how long your string is set, it won't affect the encoding size (which is usually 1 bit). +::: + +### Literal Type + +```ts +type A = 'AAA' | 'BBB' | 'CCC' +type B = 1 | 2 +type C = true | null | undefined +``` + +:::tip +Like `enum`, static text types do not affect the encoding size (usually 1 bit) no matter how large or long you set them, which can definitely reduce the package size significantly. +::: + +### Any Type + +```ts +type A = any +``` + +### Date Type + +i.e. JavaScript's native `Date` + +```ts +type A = Date +``` + +## Arrays and objects + +### Interface Type + +Both `` type`'' and ``interface`' are supported. + +```ts +type A = { + a: string + b?: number +} + +interface B { + a: string + b: number + c?: { + c1: string + c2: boolean + } +} +``` + +Also supports the definition of Index Sigature. + +```ts +interface A { + a: string + b: number + [key: string]: string | number +} + +type B = { + [key: number]: { value: string } +} +``` + +Inheritance is also supported. + +```ts +interface A { + a: string +} + +interface B extends A { + b: string +} +``` + +### Array Type + +Both ways of writing arrays are supported. + +```ts +type A = string[] +type B = Array +``` + +### Tuple Type + +```ts +type A = [number, string] +type B = [string, number?, boolean?] +``` + +## Reference types + +### Type references + +The same file reference naturally does not matter, but also supports cross-file references ~ + +```ts title="A.ts" +export type A = { value: string } +``` + +```ts title="B.ts" +import { A } from '. /A' + +type B = A +``` + +### field references + +```ts +type A = { + aaa: string + bbb: { ccc: number } +} + +// see here ↓ +type B = A['bbb'] +// You can nest them if you want +type C = A['bbb']['ccc'] +``` + +## Logical types + +### Union Type + +```ts +type A = string | number +// can be nested at will +type A = { type: 'aaa'; aaa: string } | { type: 'bbb'; bbb: string } +``` + +### Intersection Type + +```ts +type A = { a: string } & { b: string } +type X = A & B & C +``` + +:::tip +`Union Type` can be arbitrarily nested with `Intersection Type`, e.g. `A & ( B | C )`. +::: + +## Tool types + +### Non Primitive Type + +Same as TypeScript's own ``NonPrimitive` + +```ts +type A = NonPrimitive +``` + +### Pick Type + +Same as `Pick` that comes with TypeScript + +```ts +interface A { + a: string + b: number + c: boolean[] +} + +// { a: string, b: number } +type B = Pick +``` + +### Omit Type + +Same as TypeScript's own ``Omit` + +```ts +interface A { + a: string + b: number + c: boolean[] +} + +// { c: boolean[] } +type B = Omit +``` + +### Overwrite Type + +This is a tool type defined by TSBuffer itself for rewriting parts of `interface`, and needs to be introduced from `tsrpc-proto`, e.g. + +```ts +import { overwrite } from 'tsrpc-proto' + +interface A { + a: string + b: number + c: boolean[] +} + +// { a: string, b: number, c: number, d: number } +type B = Overwrite< + A, + { + c: number + d: number + } +> +``` + +## Combining and nesting + +All of the above supported types can be spliced, combined, nested, and all of them are not in the picture, not only can you detect the type, but you can also binary code it. + +For example. + +```ts +type X1 = { + value?: Array< + { common: string } & ( + | { + type: 'A' + a: string + } + | { + type: 'B' + b: string + } + ) + > | null +} +``` + +More complex can be parsed. + +## Binary encoding algorithm + +The binary encoding is not `JSON.stringify`, but a true binary encoding with efficiency equivalent to ProtoBuf. + +Interested to know about my another independent open source project [TSBuffer](https://github.com/k8w/tsbuffer). diff --git a/i18n/en/docusaurus-plugin-content-docs/current/temp/development/_category_.json b/i18n/en/docusaurus-plugin-content-docs/current/temp/development/_category_.json new file mode 100644 index 0000000..2bf488c --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-docs/current/temp/development/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "开发指南", + "position": 3 +} \ No newline at end of file diff --git a/i18n/en/docusaurus-plugin-content-docs/current/temp/development/client.md b/i18n/en/docusaurus-plugin-content-docs/current/temp/development/client.md new file mode 100644 index 0000000..b5a0aad --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-docs/current/temp/development/client.md @@ -0,0 +1,25 @@ +--- +sidebar_position: 5 +--- + +# Client Client + +:::danger WIP +This document is still in progress ...... Please look forward to it. +::: + +## Supported Platforms + +The TSRPC client supports multiple platforms and can be created from different NPM packages as needed + +- NodeJS: `tsrpc` +- Browser, ReactNative: `tsrpc-browser` +- WeChat applet: `tsrpc-miniapp` + +## callApi and cancel + +``ts + +``` + +``` diff --git a/i18n/en/docusaurus-plugin-content-docs/current/temp/development/project-structure.md b/i18n/en/docusaurus-plugin-content-docs/current/temp/development/project-structure.md new file mode 100644 index 0000000..2fe87c8 --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-docs/current/temp/development/project-structure.md @@ -0,0 +1,25 @@ +--- +sidebar_position: 1 +--- + +# Project structure + +:::danger WIP +This document is still being written ...... Stay tuned. +::: + +## list of `create-tsrpc-app` templates + +## Common directory structure + +## Environment and configuration + +## Shared code + +### Manual synchronization + +## Automatic synchronization via Symlink + +## VS Code usage suggestions + +Open a separate project window and set a different color diff --git a/i18n/en/docusaurus-plugin-content-docs/current/temp/development/server.md b/i18n/en/docusaurus-plugin-content-docs/current/temp/development/server.md new file mode 100644 index 0000000..f7fdc88 --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-docs/current/temp/development/server.md @@ -0,0 +1,67 @@ +--- +sidebar_position: 4 +--- + +# Server server + +:::danger WIP +This document is still in progress ...... Stay tuned. +::: + +## Three-tier architecture + +- Server +- Connection +- Call + +## Start-up process + +- Prepare everything, then `server.start()` + +## Return an error + +- Business Error +- Server Internal Error + +### Additional error messages + +`call.error` also supports passing a second parameter to return additional error information (e.g. error codes, etc.), e.g. + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + + + + + +```ts +export async function ApiXXXX(call: ApiCall) { + call.error( + 'This is a special error that needs to be identified exactly by the error code.', + { + code: 'XXX_ERR', + someThingElse: 'xxxxxxx', + } + ) +} +``` + + + + + +```ts +let ret = await client.callApi('XXXX', { ... }); +if(ret.err?.code === 'XXX_ERR'){ + // do some thing + console.log(ret.err.someThingElse); +} +``` + + + diff --git a/i18n/en/docusaurus-plugin-content-docs/current/temp/development/service-proto.md b/i18n/en/docusaurus-plugin-content-docs/current/temp/development/service-proto.md new file mode 100644 index 0000000..dd01496 --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-docs/current/temp/development/service-proto.md @@ -0,0 +1,15 @@ +--- +sidebar_position: 2 +--- + +# ServiceProto protocol + +:::danger WIP +This document is still in progress ...... Stay tuned. +::: + +## Protocol and message definitions + +## ServiceProto generation + +## ServiceProto update diff --git a/i18n/en/docusaurus-plugin-content-docs/current/temp/development/service.md b/i18n/en/docusaurus-plugin-content-docs/current/temp/development/service.md new file mode 100644 index 0000000..6bda96b --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-docs/current/temp/development/service.md @@ -0,0 +1,19 @@ +--- +sidebar_position: 3 +--- + +# Service + +:::danger WIP +This document is still in progress ...... Stay tuned. +::: + +## API Service + +## Message Service + +## Get Service configuration + +### Server + +### Client diff --git a/i18n/en/docusaurus-plugin-content-docs/current/temp/development/tips-for-mongodb.md b/i18n/en/docusaurus-plugin-content-docs/current/temp/development/tips-for-mongodb.md new file mode 100644 index 0000000..cb7cd92 --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-docs/current/temp/development/tips-for-mongodb.md @@ -0,0 +1,15 @@ +--- +sidebar_position: 7.1 +--- + +# MongoDB usage suggestions + +:::danger WIP +This document is still in progress ...... Stay tuned. +::: + +## strictNullChecks + +## Collection type Map + +## Share type definitions with Overwrite diff --git a/i18n/en/docusaurus-plugin-content-docs/current/temp/development/tsrpc-cli.md b/i18n/en/docusaurus-plugin-content-docs/current/temp/development/tsrpc-cli.md new file mode 100644 index 0000000..179ffe8 --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-docs/current/temp/development/tsrpc-cli.md @@ -0,0 +1,9 @@ +--- +sidebar_position: 10 +--- + +# tsrpc-cli command line tools + +:::danger WIP +This documentation is still in progress ...... Stay tuned. +::: diff --git a/i18n/en/docusaurus-plugin-content-docs/current/temp/development/vscode.md b/i18n/en/docusaurus-plugin-content-docs/current/temp/development/vscode.md new file mode 100644 index 0000000..d01ccbe --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-docs/current/temp/development/vscode.md @@ -0,0 +1,11 @@ +--- +sidebar_position: 7 +--- + +# Suggestions for using VS Code + +- Configuring breakpoint debugging in VS Code + +:::danger WIP +This documentation is still in progress ...... Stay tuned. +::: diff --git a/i18n/en/docusaurus-plugin-content-docs/current/temp/manual-creation.md b/i18n/en/docusaurus-plugin-content-docs/current/temp/manual-creation.md new file mode 100644 index 0000000..22337c6 --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-docs/current/temp/manual-creation.md @@ -0,0 +1,41 @@ +## Manual creation + +### Server + +TSRPC framework. + +```shell +npm install tsrpc +``` + +Command line tools for generating protocols: (generally used under back-end projects) + +```shell +npm install tsrpc-cli --save-dev +``` + +### Client + +#### NodeJS + +```shell +npm install tsrpc +``` + +#### Browser + +```shell +npm install tsrpc-browser +``` + +#### applet + +```shell +npm install tsrpc-miniapp +``` + +#### React Native + +```shell +npm install tsrpc-browser +``` diff --git a/i18n/en/docusaurus-plugin-content-docs/current/temp/the-first-api.md b/i18n/en/docusaurus-plugin-content-docs/current/temp/the-first-api.md new file mode 100644 index 0000000..9c88318 --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-docs/current/temp/the-first-api.md @@ -0,0 +1,134 @@ +--- +sidebar_position: 2 +--- + +# First API + +The API is essentially an **asynchronous function** that is implemented on the Server side and called on the Client side. + +The input parameter to this asynchronous function is called a request (`Request`) and the output is called a response (`Response`). +The file that defines a request and response pair is called a `Protocol`. + +To implement an API, you first need to define its protocol. + +## Defining protocols + +### Naming convention + +All protocols are located in the `backend/src/shared/protocols` directory, which is called the **protocols directory**. + +All files in the protocols directory and its subdirectories named `Ptl*.ts` are considered **protocols**, and `*` is resolved to **protocols name**, e.g. + +| protocol file path | protocol name | +| ---------------------------- | ------------- | +| `protocols/PtlGetData.ts` | `GetData` | +| `protocols/user/PtlLogin.ts` | `Login` | + +:::note + +- The prefix `Ptl` stands for the abbreviation `Protocol`. +- The protocol name depends only on the filename and is not affected by the path where it is located. + ::: + +### Requests and responses + +A protocol file corresponds to an API interface. Therefore, within the protocol file, there must be 2 `export` type definitions. + +- Request: `Req${protocol name}` +- Response: `Res${protocol name}` + +:::note + +- `Req` is an abbreviation for `Request` and `Res` is an abbreviation for `Response`. +- The type definition can be either `interface` or `type`. + ::: + +Suppose we want to implement an interface `HelloWorld`, create the file `PtlHelloWorld.ts` in the protocol directory, for example. + +```ts title="backend/src/shared/protocols/PtlHelloWorld.ts" +export interface ReqHelloWorld { + name: string +} + +export type ResHelloWorld = { + reply: string + time: Date +} +``` + +As you can see, TSRPC uses the original TypeScript to define the protocol without additional comments, and also supports more types (e.g. `Date`, `ArrayBuffer`, `Uint8Array`, etc.) + +### Generate ServiceProto + +`ServiceProto` is the real protocol definition file for TSRPC, execute the following command to generate it automatically. + +```shell +cd backend +npm run proto +``` + +:::tip +TSRPC works based on ServiceProto, so this procedure should be re-run whenever the protocol is changed. +::: + +### Shared code + +Now you see a `proto.ts` generated into the `backend/src/shared/protocols` directory, this file is the entire contents of the protocol. +Front-end projects also need these files to work properly, as well as to get better code hints. +So, we need to synchronize the protocols directory `protocols` to the front-end project. + +Further, you may have other code that can be reused between the front and back ends. For example, business code such as form validation rules, date formatting methods, etc. So we designed the `src/shared` directory, which represents all the code shared between the front and back ends. There are many ways to implement the sharing and synchronization mechanism, we default to manual synchronization: 1. + +1. the contents of `src/shared` should be edited in `backend` and then synchronized to `frontend` (read-only). +2. Execute `npm run sync` under `backend` to complete the sync manually. + +```shell +cd backend +npm run sync +``` + +:::tip +You can also use a soft link (Symlink) or other tool to automate syncing. +::: + +At this point, the API protocol is defined, generated, and synced. + +## Implementing the API + +### Naming convention + +The entry files of API interfaces are all located in `backend/src/api` directory, which correspond to the protocol files in the protocol directory one by one, except that the prefix `Ptl` is changed to `Api`. + +For example, the protocol directory file structure is as follows. + +``` +|- backend/src/shared/protocols + |- user + |- PtlLogin.ts protocol user/Login + |- PtlGetData.ts protocol GetData + |- SomeOtherFile.ts is not considered a protocol because the file name is not prefixed with Ptl +``` + +then the corresponding API implementation directory structure should be + +``` +|- backend/src/api + |- user + |- ApiLogin.ts protocol user/Login implementation + |- ApiGetData.ts implementation of the GetData protocol +``` + +### Naming convention + +As with the **protocol directory**, the + +## Calling the API + +### Call path + +The client calls the remote API based on the **call path**, which is `protocol path/protocol name`, for example, as follows + +| protocol file path | call path | +| ------------------------------------- | ------------ | +| `Protocol directory/PtlGetData.ts` | `GetData` | +| `Protocol directory/user/PtlLogin.ts` | `user/Login` | diff --git a/i18n/en/docusaurus-plugin-content-docs/current/temp/type-system.md b/i18n/en/docusaurus-plugin-content-docs/current/temp/type-system.md new file mode 100644 index 0000000..413611b --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-docs/current/temp/type-system.md @@ -0,0 +1,5 @@ +--- +sidebar_position: 1 +--- + +# Amazing Type Systems diff --git a/i18n/en/docusaurus-plugin-content-docs/current/temp/use-database.md b/i18n/en/docusaurus-plugin-content-docs/current/temp/use-database.md new file mode 100644 index 0000000..05422ed --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-docs/current/temp/use-database.md @@ -0,0 +1,88 @@ +--- +sidebar_position: 4 +--- + +- Engineering Suggestions + - Start-up process + - Environment and configuration + - Code structure + +## Using the database + +# # Choose the right database + +There are many suitable databases you can choose from, common ones such as + +- [MongoDB](https://mongodb.com): pure API call, no need to learn SQL, good combination with TypeScript. +- MySQL](https://mysql.com): classic old school SQL database. +- Redis](https://redis.io): In-memory data storage, suitable for caching and other scenarios. + +Compared to SQL databases, we recommend NoSQL databases like MongoDB, which is more suitable for code-first development using TypeScript + NodeJS. + +## Global Instances + +Usually, you can create database instances right at the start. +For example, you can place a global instance of ``export`''. + +```ts title="backend/src/index.ts +export const mongo = new MongoDB({...}) +``` + +Directly reference the global database instance when needed. + +```ts title="backend/src/api/ApiXXXX.ts +import { mongo } from '... /index' + +await mongo.collection('xxx').find({}).toArray() +``` + +## Start-up process + +Most databases need to be connected before they can be used. To ensure that everything is ready when the user's request arrives, you can handle the Server startup process yourself. +For example, connect all database instances first, then `server.start()`. Assuming that our service uses both `MongoDB` and `Redis`, you could handle the startup process like this. + +```ts title="backend/src/index.ts" + +let server = new HttpServer(); + +// global database instance +export const redis = new Redis({...}) ; +export const mongo = new Mongo.DB({...}) ; + +// Start the process +async function main(){ + // first connect to the database + await redis.connect(); + await mongo.connect(); + + // then start the service + await server.start(); +} +main().catch(e=>{ + // Any start process exception, exit the process + server.logger.error('Failed to start', e); + process.exit(-1); +}); + +``` + +## Configuration + +Many times, especially between environments, you need to modify the connection configuration of the database. +There are many mechanisms to implement them. +For example. + +- Extracting a configuration file +- Get it via environment variables + +## Code structure + +Putting configuration, public global instances, and service startup processes all in `index.ts` seems bloated and unintuitive. You can organize and disperse them into different module files, e.g. + +``` +|- backend/src + |- models + |- global.ts + |- api + |- index.ts +```