├── .editorconfig ├── .github └── workflows │ └── publish-to-npm.yml ├── .gitignore ├── .vscode └── settings.json ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── deno.json ├── generate.sh ├── import_map.json ├── scripts └── build-npm-packages │ ├── buildWrp.ts │ ├── buildWrpJotai.ts │ ├── buildWrpReact.ts │ ├── entrypoint.ts │ └── index.ts └── src ├── channel.ts ├── compat └── std │ └── io │ └── buffer.ts ├── generated ├── index.ts └── messages │ ├── index.ts │ └── pbkit │ ├── index.ts │ └── wrp │ ├── WrpGuestMessage_ReqFinish.ts │ ├── WrpGuestMessage_ReqPayload.ts │ ├── WrpGuestMessage_ReqStart.ts │ ├── WrpGuestMessage_ResFinish.ts │ ├── WrpHostMessage_Error.ts │ ├── WrpHostMessage_Initialize.ts │ ├── WrpHostMessage_ResFinish.ts │ ├── WrpHostMessage_ResPayload.ts │ ├── WrpHostMessage_ResStart.ts │ ├── WrpMessage.ts │ └── index.ts ├── glue ├── android.ts ├── child-window.ts ├── iframe.ts ├── index.ts ├── ios.ts ├── misc │ ├── util.ts │ └── web.ts ├── parent-window.ts └── parent.ts ├── guest.ts ├── host.ts ├── jotai ├── iframe.ts ├── index.ts ├── parent.ts └── pwasa.ts ├── metadata.ts ├── misc.ts ├── react ├── index.ts ├── server.ts ├── useOnceEffect.ts ├── useWrpClientImpl.ts ├── useWrpIframeSocket.ts └── useWrpParentSocket.ts ├── rpc ├── client.ts ├── misc.ts └── server.ts ├── socket.ts ├── tee.ts └── wrp.proto /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | end_of_line = lf 3 | insert_final_newline = true 4 | 5 | [*.{proto,ts,js,json,sh}] 6 | charset = utf-8 7 | indent_style = space 8 | indent_size = 2 9 | -------------------------------------------------------------------------------- /.github/workflows/publish-to-npm.yml: -------------------------------------------------------------------------------- 1 | name: Publish to NPM 2 | on: 3 | release: 4 | types: [released] 5 | 6 | jobs: 7 | publish-to-npm: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: actions/setup-node@v2 12 | with: 13 | node-version: 17.x 14 | registry-url: https://registry.npmjs.org 15 | - uses: denoland/setup-deno@v1 16 | with: 17 | deno-version: v1.x 18 | - run: deno task build:npm 19 | - run: npm publish --access=public 20 | working-directory: tmp/npm/wrp 21 | env: 22 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 23 | - run: npm publish --access=public 24 | working-directory: tmp/npm/wrp-react 25 | env: 26 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 27 | - run: npm publish --access=public 28 | working-directory: tmp/npm/wrp-jotai 29 | env: 30 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /tmp 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enable": true, 3 | "deno.lint": false, 4 | "deno.unstable": true, 5 | "deno.importMap": "./import_map.json", 6 | "editor.formatOnSave": true, 7 | "editor.defaultFormatter": "denoland.vscode-deno", 8 | "[typescript]": { 9 | "editor.defaultFormatter": "denoland.vscode-deno" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WRP - Webview/Worker Request Protocol 2 | 3 | pronounce as **wrap**(`ræp`) 4 | 5 | ## Design 6 | 7 | architecture diagram 11 | 12 | - _Glue_ - Platform-specific code to implement sockets 13 | - _Socket_ - Way for sending and receiving data between the two platforms 14 | - _Channel_ - An interface that allows information to be exchanged in units of 15 | messages using sockets. 16 | - _Guest_ - An interface that can send requests to hosts using channels. 17 | - _Host_ - An interface that allows you to process and respond to requests 18 | received from guests. 19 | 20 | wrp channel packet diagram 23 | 24 | Whenever the channel sends a message, it writes the message size as a 4-byte 25 | little-endian integer to the socket, and then writes a message payload as that 26 | size. 27 | 28 | The message payload is defined in the [./src/wrp.proto](./src/wrp.proto) file. 29 | 30 | ```mermaid 31 | sequenceDiagram 32 | participant H as WrpHost 33 | participant G as WrpGuest 34 | H->>G: HostInitialize 35 | Note over H, G: Send available methods 36 | loop 37 | G->>+H: GuestReqStart 38 | H-->>G: HostResStart 39 | par request 40 | loop 41 | G-->>H: GuestReqPayload 42 | end 43 | G-->>H: GuestReqFinish 44 | end 45 | par response 46 | loop 47 | H-->>G: HostResPayload 48 | end 49 | H-->>-G: HostResFinish 50 | end 51 | end 52 | ``` 53 | 54 | ### Types 55 | 56 | ```typescript 57 | // Glue implementation provides socket object. 58 | type Socket = Reader & Writer; 59 | interface Reader { 60 | read(p: Uint8Array): Promise; 61 | } 62 | interface Writer { 63 | write(p: Uint8Array): Promise; 64 | } 65 | 66 | // Channel provides a per-message communication method. 67 | interface WrpChannel { 68 | listen(): AsyncGenerator; 69 | send(message: WrpMessage): Promise; 70 | } 71 | 72 | type Metadata = Record; 73 | interface LazyMetadata { 74 | [key: string]: 75 | | undefined 76 | | string 77 | | Promise 78 | | (() => string | undefined) 79 | | (() => Promise); 80 | } 81 | 82 | // Guest provides a way to send requests to the host. 83 | interface WrpGuest { 84 | availableMethods: Set; 85 | request( 86 | methodName: string, 87 | req: AsyncGenerator, 88 | metadata?: LazyMetadata, 89 | ): { 90 | res: AsyncGenerator; 91 | header: Promise; 92 | trailer: Promise; 93 | }; 94 | } 95 | 96 | // Host provides a way to handle and respond to requests from guests. 97 | interface WrpHost { 98 | listen(): AsyncGenerator; 99 | } 100 | interface WrpRequest { 101 | methodName: string; 102 | metadata: Metadata; 103 | req: AsyncGenerator; 104 | sendHeader(value: Metadata): void; 105 | sendPayload(value: Uint8Array): void; 106 | sendTrailer(value: Metadata): void; 107 | } 108 | ``` 109 | -------------------------------------------------------------------------------- /deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "tasks": { 3 | "build:npm": "deno run -A --no-check scripts/build-npm-packages/entrypoint.ts", 4 | "fmt": "deno fmt --ignore=src/generated,tmp" 5 | }, 6 | "compilerOptions": { 7 | "lib": ["deno.ns", "dom"] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /generate.sh: -------------------------------------------------------------------------------- 1 | rm -rf src/generated 2 | pb gen ts --runtime-package="https://deno.land/x/pbkit@v0.0.45/core/runtime" -o src/generated src/wrp.proto 3 | -------------------------------------------------------------------------------- /import_map.json: -------------------------------------------------------------------------------- 1 | { 2 | "imports": { 3 | "react": "https://esm.sh/react@18", 4 | "jotai": "https://esm.sh/jotai@1.7.5", 5 | "jotai/utils": "https://esm.sh/jotai@1.7.5/utils" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /scripts/build-npm-packages/buildWrp.ts: -------------------------------------------------------------------------------- 1 | import { ensureDir } from "https://deno.land/std@0.137.0/fs/mod.ts"; 2 | import { walk } from "https://deno.land/std@0.137.0/fs/walk.ts"; 3 | import { 4 | dirname, 5 | join, 6 | relative, 7 | } from "https://deno.land/std@0.137.0/path/mod.ts"; 8 | import { 9 | BuildConfig, 10 | isPbkitPath, 11 | isStdPath, 12 | NameAndVersion, 13 | removeExt, 14 | replacePbkitRuntimePath, 15 | replaceStdPath, 16 | rewriteModulePath, 17 | } from "./index.ts"; 18 | 19 | export default async function buildWrp(config: BuildConfig) { 20 | const packageJson = getPackageJson(config); 21 | const tsDir = `${config.tmp}/ts`; 22 | await ensureDir(config.dist); 23 | await Deno.writeTextFile( 24 | `${config.dist}/package.json`, 25 | JSON.stringify(packageJson, null, 2) + "\n", 26 | ); 27 | await Promise.all([ 28 | Deno.copyFile("LICENSE-MIT", `${config.dist}/LICENSE-MIT`), 29 | Deno.copyFile("LICENSE-APACHE", `${config.dist}/LICENSE-APACHE`), 30 | Deno.copyFile("README.md", `${config.dist}/README.md`), 31 | ]); 32 | { // copy ts files 33 | const srcDir = "src"; 34 | const entries = walk(srcDir, { includeDirs: false, exts: [".ts"] }); 35 | for await (const { path: fromPath } of entries) { 36 | const path = fromPath.substring(srcDir.length + 1); 37 | if (path.startsWith("react") || path.startsWith("jotai")) continue; 38 | const toPath = join(tsDir, path); 39 | await ensureDir(dirname(toPath)); 40 | const code = await Deno.readTextFile(fromPath); 41 | await Deno.writeTextFile( 42 | toPath, 43 | rewriteModulePath(code, (modulePath) => { 44 | if (modulePath.startsWith(".")) { 45 | return removeExt(modulePath); 46 | } else if (isStdPath(modulePath)) { 47 | const relativePath = relative( 48 | dirname(path), 49 | replaceStdPath(modulePath), 50 | ); 51 | if (!relativePath.startsWith(".")) return `./${relativePath}`; 52 | return relativePath; 53 | } else if (isPbkitPath(modulePath)) { 54 | return replacePbkitRuntimePath(modulePath); 55 | } else { 56 | return modulePath; 57 | } 58 | }), 59 | ); 60 | } 61 | } 62 | { // tsc 63 | const entries = walk(tsDir, { includeDirs: false, exts: [".ts"] }); 64 | const tsFiles: string[] = []; 65 | for await (const { path } of entries) tsFiles.push(path); 66 | await Deno.run({ 67 | stdout: "null", 68 | stderr: "null", 69 | cmd: [ 70 | "tsc", 71 | "-m", 72 | "commonjs", 73 | "--target", 74 | "es2019", 75 | "--lib", 76 | "es2019,dom", 77 | "--declaration", 78 | "--rootDir", 79 | tsDir, 80 | "--outDir", 81 | config.dist, 82 | ...tsFiles, 83 | ], 84 | }).status(); 85 | } 86 | } 87 | 88 | export function getPackageJson(config: NameAndVersion) { 89 | const { name, version } = config; 90 | return { 91 | name, 92 | version, 93 | author: "JongChan Choi ", 94 | license: "(MIT OR Apache-2.0)", 95 | repository: { 96 | type: "git", 97 | url: "git+https://github.com/pbkit/wrp-ts.git", 98 | }, 99 | dependencies: { 100 | "@pbkit/runtime": "^0.0.45", 101 | }, 102 | }; 103 | } 104 | -------------------------------------------------------------------------------- /scripts/build-npm-packages/buildWrpJotai.ts: -------------------------------------------------------------------------------- 1 | import { ensureDir } from "https://deno.land/std@0.137.0/fs/mod.ts"; 2 | import { walk } from "https://deno.land/std@0.137.0/fs/walk.ts"; 3 | import { 4 | dirname, 5 | join, 6 | relative, 7 | } from "https://deno.land/std@0.137.0/path/mod.ts"; 8 | import { 9 | BuildConfig, 10 | isPbkitPath, 11 | isStdPath, 12 | NameAndVersion, 13 | removeExt, 14 | replacePbkitRuntimePath, 15 | replaceStdPath, 16 | rewriteModulePath, 17 | } from "./index.ts"; 18 | 19 | export default async function buildWrpJotai(config: BuildConfig) { 20 | const packageJson = getPackageJson(config); 21 | const tsDir = `${config.tmp}/ts`; 22 | await ensureDir(config.dist); 23 | await Deno.writeTextFile( 24 | `${config.dist}/package.json`, 25 | JSON.stringify(packageJson, null, 2) + "\n", 26 | ); 27 | await Promise.all([ 28 | Deno.copyFile("LICENSE-MIT", `${config.dist}/LICENSE-MIT`), 29 | Deno.copyFile("LICENSE-APACHE", `${config.dist}/LICENSE-APACHE`), 30 | Deno.copyFile("README.md", `${config.dist}/README.md`), 31 | ]); 32 | { // copy ts files 33 | const srcDir = "src/jotai"; 34 | const entries = walk(srcDir, { includeDirs: false, exts: [".ts"] }); 35 | for await (const { path: fromPath } of entries) { 36 | const path = fromPath.substring(srcDir.length + 1); 37 | const toPath = join(tsDir, path); 38 | await ensureDir(dirname(toPath)); 39 | const code = await Deno.readTextFile(fromPath); 40 | await Deno.writeTextFile( 41 | toPath, 42 | rewriteModulePath(code, (modulePath) => { 43 | if (modulePath.startsWith("../react/")) { 44 | return removeExt( 45 | modulePath.replace(/^\.\.\/react\//, "@pbkit/wrp-react/"), 46 | ); 47 | } else if (modulePath.startsWith("../")) { 48 | return removeExt(modulePath.replace(/^\.\.\//, "@pbkit/wrp/")); 49 | } else if (modulePath.startsWith(".")) { 50 | return removeExt(modulePath); 51 | } else if (isStdPath(modulePath)) { 52 | const relativePath = relative( 53 | dirname(path), 54 | replaceStdPath(modulePath), 55 | ); 56 | if (!relativePath.startsWith(".")) return `./${relativePath}`; 57 | return relativePath; 58 | } else if (isPbkitPath(modulePath)) { 59 | return replacePbkitRuntimePath(modulePath); 60 | } else { 61 | return modulePath; 62 | } 63 | }), 64 | ); 65 | } 66 | } 67 | { // tsc 68 | const entries = walk(tsDir, { includeDirs: false, exts: [".ts"] }); 69 | const tsFiles: string[] = []; 70 | for await (const { path } of entries) tsFiles.push(path); 71 | await Deno.run({ 72 | stdout: "null", 73 | stderr: "null", 74 | cmd: [ 75 | "tsc", 76 | "-m", 77 | "commonjs", 78 | "--target", 79 | "es2019", 80 | "--lib", 81 | "es2019,dom", 82 | "--declaration", 83 | "--rootDir", 84 | tsDir, 85 | "--outDir", 86 | config.dist, 87 | ...tsFiles, 88 | ], 89 | }).status(); 90 | } 91 | } 92 | 93 | export function getPackageJson(config: NameAndVersion) { 94 | const { name, version } = config; 95 | return { 96 | name, 97 | version, 98 | author: "JongChan Choi ", 99 | license: "(MIT OR Apache-2.0)", 100 | repository: { 101 | type: "git", 102 | url: "git+https://github.com/pbkit/wrp-ts.git", 103 | }, 104 | peerDependencies: { 105 | "@pbkit/wrp": "*", 106 | "jotai": "*", 107 | }, 108 | }; 109 | } 110 | -------------------------------------------------------------------------------- /scripts/build-npm-packages/buildWrpReact.ts: -------------------------------------------------------------------------------- 1 | import { ensureDir } from "https://deno.land/std@0.137.0/fs/mod.ts"; 2 | import { walk } from "https://deno.land/std@0.137.0/fs/walk.ts"; 3 | import { 4 | dirname, 5 | join, 6 | relative, 7 | } from "https://deno.land/std@0.137.0/path/mod.ts"; 8 | import { 9 | BuildConfig, 10 | isPbkitPath, 11 | isStdPath, 12 | NameAndVersion, 13 | removeExt, 14 | replacePbkitRuntimePath, 15 | replaceStdPath, 16 | rewriteModulePath, 17 | } from "./index.ts"; 18 | 19 | export default async function buildWrpReact(config: BuildConfig) { 20 | const packageJson = getPackageJson(config); 21 | const tsDir = `${config.tmp}/ts`; 22 | await ensureDir(config.dist); 23 | await Deno.writeTextFile( 24 | `${config.dist}/package.json`, 25 | JSON.stringify(packageJson, null, 2) + "\n", 26 | ); 27 | await Promise.all([ 28 | Deno.copyFile("LICENSE-MIT", `${config.dist}/LICENSE-MIT`), 29 | Deno.copyFile("LICENSE-APACHE", `${config.dist}/LICENSE-APACHE`), 30 | Deno.copyFile("README.md", `${config.dist}/README.md`), 31 | ]); 32 | { // copy ts files 33 | const srcDir = "src/react"; 34 | const entries = walk(srcDir, { includeDirs: false, exts: [".ts"] }); 35 | for await (const { path: fromPath } of entries) { 36 | const path = fromPath.substring(srcDir.length + 1); 37 | const toPath = join(tsDir, path); 38 | await ensureDir(dirname(toPath)); 39 | const code = await Deno.readTextFile(fromPath); 40 | await Deno.writeTextFile( 41 | toPath, 42 | rewriteModulePath(code, (modulePath) => { 43 | if (modulePath.startsWith("../")) { 44 | return removeExt(modulePath.replace(/^\.\.\//, "@pbkit/wrp/")); 45 | } else if (modulePath.startsWith(".")) { 46 | return removeExt(modulePath); 47 | } else if (isStdPath(modulePath)) { 48 | const relativePath = relative( 49 | dirname(path), 50 | replaceStdPath(modulePath), 51 | ); 52 | if (!relativePath.startsWith(".")) return `./${relativePath}`; 53 | return relativePath; 54 | } else if (isPbkitPath(modulePath)) { 55 | return replacePbkitRuntimePath(modulePath); 56 | } else { 57 | return modulePath; 58 | } 59 | }), 60 | ); 61 | } 62 | } 63 | { // tsc 64 | const entries = walk(tsDir, { includeDirs: false, exts: [".ts"] }); 65 | const tsFiles: string[] = []; 66 | for await (const { path } of entries) tsFiles.push(path); 67 | await Deno.run({ 68 | stdout: "null", 69 | stderr: "null", 70 | cmd: [ 71 | "tsc", 72 | "-m", 73 | "commonjs", 74 | "--target", 75 | "es2019", 76 | "--lib", 77 | "es2019,dom", 78 | "--declaration", 79 | "--rootDir", 80 | tsDir, 81 | "--outDir", 82 | config.dist, 83 | ...tsFiles, 84 | ], 85 | }).status(); 86 | } 87 | } 88 | 89 | export function getPackageJson(config: NameAndVersion) { 90 | const { name, version } = config; 91 | return { 92 | name, 93 | version, 94 | author: "JongChan Choi ", 95 | license: "(MIT OR Apache-2.0)", 96 | repository: { 97 | type: "git", 98 | url: "git+https://github.com/pbkit/wrp-ts.git", 99 | }, 100 | peerDependencies: { 101 | "@pbkit/wrp": "*", 102 | "react": "*", 103 | }, 104 | }; 105 | } 106 | -------------------------------------------------------------------------------- /scripts/build-npm-packages/entrypoint.ts: -------------------------------------------------------------------------------- 1 | import { emptyDir } from "https://deno.land/std@0.122.0/fs/mod.ts"; 2 | import buildWrp from "./buildWrp.ts"; 3 | import buildWrpJotai from "./buildWrpJotai.ts"; 4 | import buildWrpReact from "./buildWrpReact.ts"; 5 | 6 | await emptyDir("tmp/npm"); 7 | 8 | const latestTag = new TextDecoder().decode( 9 | await Deno.run({ 10 | cmd: ["git", "describe", "--tags", "--abbrev=0"], 11 | stdout: "piped", 12 | }).output(), 13 | ); 14 | const version = latestTag.slice(1).trim(); 15 | 16 | await Promise.all([ 17 | buildWrp({ 18 | name: "@pbkit/wrp", 19 | version, 20 | dist: "tmp/npm/wrp", 21 | tmp: "tmp/npm/tmp/wrp", 22 | }), 23 | buildWrpReact({ 24 | name: "@pbkit/wrp-react", 25 | version, 26 | dist: "tmp/npm/wrp-react", 27 | tmp: "tmp/npm/tmp/wrp-react", 28 | }), 29 | buildWrpJotai({ 30 | name: "@pbkit/wrp-jotai", 31 | version, 32 | dist: "tmp/npm/wrp-jotai", 33 | tmp: "tmp/npm/tmp/wrp-jotai", 34 | }), 35 | ]); 36 | -------------------------------------------------------------------------------- /scripts/build-npm-packages/index.ts: -------------------------------------------------------------------------------- 1 | export interface BuildConfig extends NameAndVersion { 2 | dist: string; 3 | tmp?: string; 4 | } 5 | 6 | export interface NameAndVersion { 7 | name: string; 8 | version: string; 9 | } 10 | 11 | export function rewriteModulePath( 12 | code: string, 13 | fn: Parameters[1], 14 | ): string { 15 | // 1) import "foo"; 16 | // 2) import bar from "foo"; 17 | // 3) import { bar } from "foo"; 18 | // 4) import * as bar from "foo"; 19 | // 5) export { bar } from "foo"; 20 | // 6) export * from "foo"; 21 | // 7) } from "foo"; 22 | return code.replaceAll( 23 | /(?<=^\s*(?:import\s*|(?:(?:import|export)\b.*?\b|}\s*)from\s*)(["'])).+(?=\1)/gm, 24 | fn, 25 | ); 26 | } 27 | 28 | export function isPbkitPath(path: string): boolean { 29 | return ( 30 | path.startsWith("https://deno.land/x/pbkit") || 31 | path.startsWith("https:/deno.land/x/pbkit") 32 | ); 33 | } 34 | 35 | export function isStdPath(path: string): boolean { 36 | return path.startsWith("https://deno.land/std"); 37 | } 38 | 39 | export function replacePbkitRuntimePath(path: string): string { 40 | return removeExt( 41 | path.replace( 42 | /^https:\/\/?deno\.land\/x\/pbkit[^\/]*?\/core\/runtime/, 43 | "@pbkit/runtime", 44 | ), 45 | ); 46 | } 47 | 48 | export function replaceStdPath(path: string): string { 49 | return removeExt( 50 | path.replace( 51 | /^https:\/\/deno\.land\/std[^\/]*?\/(.+)/, 52 | "compat/std/$1", 53 | ), 54 | ); 55 | } 56 | 57 | export function removeExt(path: string): string { 58 | return path.replace(/\.[^.]+$/, ""); 59 | } 60 | -------------------------------------------------------------------------------- /src/channel.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BufReader, 3 | BufWriter, 4 | } from "https://deno.land/std@0.137.0/io/buffer.ts"; 5 | import { 6 | decodeBinary, 7 | encodeBinary, 8 | Type as WrpMessage, 9 | } from "./generated/messages/pbkit/wrp/WrpMessage.ts"; 10 | import { Socket } from "./socket.ts"; 11 | import { chain } from "./misc.ts"; 12 | 13 | export interface WrpChannel { 14 | listen(): AsyncGenerator; 15 | send(message: WrpMessage): Promise; 16 | } 17 | export function createWrpChannel(socket: Socket): WrpChannel { 18 | return { 19 | async *listen() { 20 | const bufReader = new BufReader(socket); 21 | while (true) { 22 | let lengthU8s: Uint8Array | null = null; 23 | try { 24 | lengthU8s = await bufReader.readFull(new Uint8Array(4)); 25 | } catch { /* error when socket closed */ } 26 | if (!lengthU8s) break; 27 | const length = new DataView(lengthU8s.buffer).getUint32(0, true); 28 | const payload = await bufReader.readFull(new Uint8Array(length)); 29 | if (!payload) throw new UnexpectedEof(); 30 | yield decodeBinary(payload); 31 | } 32 | }, 33 | send: chain(async function send(message) { 34 | const payload = encodeBinary(message); 35 | const lengthU8s = new Uint8Array(4); 36 | new DataView(lengthU8s.buffer).setUint32(0, payload.length, true); 37 | const bufWriter = new BufWriter(socket); 38 | await bufWriter.write(lengthU8s); 39 | await bufWriter.write(payload); 40 | await bufWriter.flush(); 41 | }), 42 | }; 43 | } 44 | 45 | export const UnexpectedEof = ( 46 | typeof Deno === "undefined" 47 | ? class UnexpectedEof extends Error {} 48 | : Deno.errors.UnexpectedEof 49 | ); 50 | -------------------------------------------------------------------------------- /src/compat/std/io/buffer.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. 2 | 3 | function copy(src: Uint8Array, dst: Uint8Array, off = 0): number { 4 | off = Math.max(0, Math.min(off, dst.byteLength)); 5 | const dstBytesAvailable = dst.byteLength - off; 6 | if (src.byteLength > dstBytesAvailable) { 7 | src = src.subarray(0, dstBytesAvailable); 8 | } 9 | dst.set(src, off); 10 | return src.byteLength; 11 | } 12 | interface Reader { 13 | read(p: Uint8Array): Promise; 14 | } 15 | interface Writer { 16 | write(p: Uint8Array): Promise; 17 | } 18 | 19 | const DEFAULT_BUF_SIZE = 4096; 20 | const MIN_BUF_SIZE = 16; 21 | const MAX_CONSECUTIVE_EMPTY_READS = 100; 22 | const CR = "\r".charCodeAt(0); 23 | const LF = "\n".charCodeAt(0); 24 | 25 | export class BufferFullError extends Error { 26 | override name = "BufferFullError"; 27 | constructor(public partial: Uint8Array) { 28 | super("Buffer full"); 29 | } 30 | } 31 | 32 | export class PartialReadError extends Error { 33 | override name = "PartialReadError"; 34 | partial?: Uint8Array; 35 | constructor() { 36 | super("Encountered UnexpectedEof, data only partially read"); 37 | } 38 | } 39 | 40 | /** Result type returned by of BufReader.readLine(). */ 41 | export interface ReadLineResult { 42 | line: Uint8Array; 43 | more: boolean; 44 | } 45 | 46 | /** BufReader implements buffering for a Reader object. */ 47 | export class BufReader implements Reader { 48 | #buf!: Uint8Array; 49 | #rd!: Reader; // Reader provided by caller. 50 | #r = 0; // buf read position. 51 | #w = 0; // buf write position. 52 | #eof = false; 53 | // private lastByte: number; 54 | // private lastCharSize: number; 55 | 56 | /** return new BufReader unless r is BufReader */ 57 | static create(r: Reader, size: number = DEFAULT_BUF_SIZE): BufReader { 58 | return r instanceof BufReader ? r : new BufReader(r, size); 59 | } 60 | 61 | constructor(rd: Reader, size: number = DEFAULT_BUF_SIZE) { 62 | if (size < MIN_BUF_SIZE) { 63 | size = MIN_BUF_SIZE; 64 | } 65 | this.#reset(new Uint8Array(size), rd); 66 | } 67 | 68 | /** Returns the size of the underlying buffer in bytes. */ 69 | size(): number { 70 | return this.#buf.byteLength; 71 | } 72 | 73 | buffered(): number { 74 | return this.#w - this.#r; 75 | } 76 | 77 | // Reads a new chunk into the buffer. 78 | #fill = async () => { 79 | // Slide existing data to beginning. 80 | if (this.#r > 0) { 81 | this.#buf.copyWithin(0, this.#r, this.#w); 82 | this.#w -= this.#r; 83 | this.#r = 0; 84 | } 85 | 86 | if (this.#w >= this.#buf.byteLength) { 87 | throw Error("bufio: tried to fill full buffer"); 88 | } 89 | 90 | // Read new data: try a limited number of times. 91 | for (let i = MAX_CONSECUTIVE_EMPTY_READS; i > 0; i--) { 92 | const rr = await this.#rd.read(this.#buf.subarray(this.#w)); 93 | if (rr === null) { 94 | this.#eof = true; 95 | return; 96 | } 97 | this.#w += rr; 98 | if (rr > 0) { 99 | return; 100 | } 101 | } 102 | 103 | throw new Error( 104 | `No progress after ${MAX_CONSECUTIVE_EMPTY_READS} read() calls`, 105 | ); 106 | }; 107 | 108 | /** Discards any buffered data, resets all state, and switches 109 | * the buffered reader to read from r. 110 | */ 111 | reset(r: Reader): void { 112 | this.#reset(this.#buf, r); 113 | } 114 | 115 | #reset = (buf: Uint8Array, rd: Reader): void => { 116 | this.#buf = buf; 117 | this.#rd = rd; 118 | this.#eof = false; 119 | // this.lastByte = -1; 120 | // this.lastCharSize = -1; 121 | }; 122 | 123 | /** reads data into p. 124 | * It returns the number of bytes read into p. 125 | * The bytes are taken from at most one Read on the underlying Reader, 126 | * hence n may be less than len(p). 127 | * To read exactly len(p) bytes, use io.ReadFull(b, p). 128 | */ 129 | async read(p: Uint8Array): Promise { 130 | let rr: number | null = p.byteLength; 131 | if (p.byteLength === 0) return rr; 132 | 133 | if (this.#r === this.#w) { 134 | if (p.byteLength >= this.#buf.byteLength) { 135 | // Large read, empty buffer. 136 | // Read directly into p to avoid copy. 137 | const rr = await this.#rd.read(p); 138 | const nread = rr ?? 0; 139 | // if (rr.nread > 0) { 140 | // this.lastByte = p[rr.nread - 1]; 141 | // this.lastCharSize = -1; 142 | // } 143 | return rr; 144 | } 145 | 146 | // One read. 147 | // Do not use this.fill, which will loop. 148 | this.#r = 0; 149 | this.#w = 0; 150 | rr = await this.#rd.read(this.#buf); 151 | if (rr === 0 || rr === null) return rr; 152 | this.#w += rr; 153 | } 154 | 155 | // copy as much as we can 156 | const copied = copy(this.#buf.subarray(this.#r, this.#w), p, 0); 157 | this.#r += copied; 158 | // this.lastByte = this.buf[this.r - 1]; 159 | // this.lastCharSize = -1; 160 | return copied; 161 | } 162 | 163 | /** reads exactly `p.length` bytes into `p`. 164 | * 165 | * If successful, `p` is returned. 166 | * 167 | * If the end of the underlying stream has been reached, and there are no more 168 | * bytes available in the buffer, `readFull()` returns `null` instead. 169 | * 170 | * An error is thrown if some bytes could be read, but not enough to fill `p` 171 | * entirely before the underlying stream reported an error or EOF. Any error 172 | * thrown will have a `partial` property that indicates the slice of the 173 | * buffer that has been successfully filled with data. 174 | * 175 | * Ported from https://golang.org/pkg/io/#ReadFull 176 | */ 177 | async readFull(p: Uint8Array): Promise { 178 | let bytesRead = 0; 179 | while (bytesRead < p.length) { 180 | try { 181 | const rr = await this.read(p.subarray(bytesRead)); 182 | if (rr === null) { 183 | if (bytesRead === 0) { 184 | return null; 185 | } else { 186 | throw new PartialReadError(); 187 | } 188 | } 189 | bytesRead += rr; 190 | } catch (err) { 191 | if (err instanceof PartialReadError) { 192 | err.partial = p.subarray(0, bytesRead); 193 | } else if (err instanceof Error) { 194 | const e = new PartialReadError(); 195 | e.partial = p.subarray(0, bytesRead); 196 | e.stack = err.stack; 197 | e.message = err.message; 198 | e.cause = err.cause; 199 | throw err; 200 | } 201 | throw err; 202 | } 203 | } 204 | return p; 205 | } 206 | 207 | /** Returns the next byte [0, 255] or `null`. */ 208 | async readByte(): Promise { 209 | while (this.#r === this.#w) { 210 | if (this.#eof) return null; 211 | await this.#fill(); // buffer is empty. 212 | } 213 | const c = this.#buf[this.#r]; 214 | this.#r++; 215 | // this.lastByte = c; 216 | return c; 217 | } 218 | 219 | /** readString() reads until the first occurrence of delim in the input, 220 | * returning a string containing the data up to and including the delimiter. 221 | * If ReadString encounters an error before finding a delimiter, 222 | * it returns the data read before the error and the error itself 223 | * (often `null`). 224 | * ReadString returns err != nil if and only if the returned data does not end 225 | * in delim. 226 | * For simple uses, a Scanner may be more convenient. 227 | */ 228 | async readString(delim: string): Promise { 229 | if (delim.length !== 1) { 230 | throw new Error("Delimiter should be a single character"); 231 | } 232 | const buffer = await this.readSlice(delim.charCodeAt(0)); 233 | if (buffer === null) return null; 234 | return new TextDecoder().decode(buffer); 235 | } 236 | 237 | /** `readLine()` is a low-level line-reading primitive. Most callers should 238 | * use `readString('\n')` instead or use a Scanner. 239 | * 240 | * `readLine()` tries to return a single line, not including the end-of-line 241 | * bytes. If the line was too long for the buffer then `more` is set and the 242 | * beginning of the line is returned. The rest of the line will be returned 243 | * from future calls. `more` will be false when returning the last fragment 244 | * of the line. The returned buffer is only valid until the next call to 245 | * `readLine()`. 246 | * 247 | * The text returned from ReadLine does not include the line end ("\r\n" or 248 | * "\n"). 249 | * 250 | * When the end of the underlying stream is reached, the final bytes in the 251 | * stream are returned. No indication or error is given if the input ends 252 | * without a final line end. When there are no more trailing bytes to read, 253 | * `readLine()` returns `null`. 254 | * 255 | * Calling `unreadByte()` after `readLine()` will always unread the last byte 256 | * read (possibly a character belonging to the line end) even if that byte is 257 | * not part of the line returned by `readLine()`. 258 | */ 259 | async readLine(): Promise { 260 | let line: Uint8Array | null = null; 261 | 262 | try { 263 | line = await this.readSlice(LF); 264 | } catch (err) { 265 | if (err instanceof Deno.errors.BadResource) { 266 | throw err; 267 | } 268 | let partial; 269 | if (err instanceof PartialReadError) { 270 | partial = err.partial; 271 | } 272 | 273 | // Don't throw if `readSlice()` failed with `BufferFullError`, instead we 274 | // just return whatever is available and set the `more` flag. 275 | if (!(err instanceof BufferFullError)) { 276 | throw err; 277 | } 278 | 279 | partial = err.partial; 280 | 281 | // Handle the case where "\r\n" straddles the buffer. 282 | if ( 283 | !this.#eof && partial && 284 | partial.byteLength > 0 && 285 | partial[partial.byteLength - 1] === CR 286 | ) { 287 | // Put the '\r' back on buf and drop it from line. 288 | // Let the next call to ReadLine check for "\r\n". 289 | this.#r--; 290 | partial = partial.subarray(0, partial.byteLength - 1); 291 | } 292 | 293 | if (partial) { 294 | return { line: partial, more: !this.#eof }; 295 | } 296 | } 297 | 298 | if (line === null) { 299 | return null; 300 | } 301 | 302 | if (line.byteLength === 0) { 303 | return { line, more: false }; 304 | } 305 | 306 | if (line[line.byteLength - 1] == LF) { 307 | let drop = 1; 308 | if (line.byteLength > 1 && line[line.byteLength - 2] === CR) { 309 | drop = 2; 310 | } 311 | line = line.subarray(0, line.byteLength - drop); 312 | } 313 | return { line, more: false }; 314 | } 315 | 316 | /** `readSlice()` reads until the first occurrence of `delim` in the input, 317 | * returning a slice pointing at the bytes in the buffer. The bytes stop 318 | * being valid at the next read. 319 | * 320 | * If `readSlice()` encounters an error before finding a delimiter, or the 321 | * buffer fills without finding a delimiter, it throws an error with a 322 | * `partial` property that contains the entire buffer. 323 | * 324 | * If `readSlice()` encounters the end of the underlying stream and there are 325 | * any bytes left in the buffer, the rest of the buffer is returned. In other 326 | * words, EOF is always treated as a delimiter. Once the buffer is empty, 327 | * it returns `null`. 328 | * 329 | * Because the data returned from `readSlice()` will be overwritten by the 330 | * next I/O operation, most clients should use `readString()` instead. 331 | */ 332 | async readSlice(delim: number): Promise { 333 | let s = 0; // search start index 334 | let slice: Uint8Array | undefined; 335 | 336 | while (true) { 337 | // Search buffer. 338 | let i = this.#buf.subarray(this.#r + s, this.#w).indexOf(delim); 339 | if (i >= 0) { 340 | i += s; 341 | slice = this.#buf.subarray(this.#r, this.#r + i + 1); 342 | this.#r += i + 1; 343 | break; 344 | } 345 | 346 | // EOF? 347 | if (this.#eof) { 348 | if (this.#r === this.#w) { 349 | return null; 350 | } 351 | slice = this.#buf.subarray(this.#r, this.#w); 352 | this.#r = this.#w; 353 | break; 354 | } 355 | 356 | // Buffer full? 357 | if (this.buffered() >= this.#buf.byteLength) { 358 | this.#r = this.#w; 359 | // #4521 The internal buffer should not be reused across reads because it causes corruption of data. 360 | const oldbuf = this.#buf; 361 | const newbuf = this.#buf.slice(0); 362 | this.#buf = newbuf; 363 | throw new BufferFullError(oldbuf); 364 | } 365 | 366 | s = this.#w - this.#r; // do not rescan area we scanned before 367 | 368 | // Buffer is not full. 369 | try { 370 | await this.#fill(); 371 | } catch (err) { 372 | if (err instanceof PartialReadError) { 373 | err.partial = slice; 374 | } else if (err instanceof Error) { 375 | const e = new PartialReadError(); 376 | e.partial = slice; 377 | e.stack = err.stack; 378 | e.message = err.message; 379 | e.cause = err.cause; 380 | throw err; 381 | } 382 | throw err; 383 | } 384 | } 385 | 386 | // Handle last byte, if any. 387 | // const i = slice.byteLength - 1; 388 | // if (i >= 0) { 389 | // this.lastByte = slice[i]; 390 | // this.lastCharSize = -1 391 | // } 392 | 393 | return slice; 394 | } 395 | 396 | /** `peek()` returns the next `n` bytes without advancing the reader. The 397 | * bytes stop being valid at the next read call. 398 | * 399 | * When the end of the underlying stream is reached, but there are unread 400 | * bytes left in the buffer, those bytes are returned. If there are no bytes 401 | * left in the buffer, it returns `null`. 402 | * 403 | * If an error is encountered before `n` bytes are available, `peek()` throws 404 | * an error with the `partial` property set to a slice of the buffer that 405 | * contains the bytes that were available before the error occurred. 406 | */ 407 | async peek(n: number): Promise { 408 | if (n < 0) { 409 | throw Error("negative count"); 410 | } 411 | 412 | let avail = this.#w - this.#r; 413 | while (avail < n && avail < this.#buf.byteLength && !this.#eof) { 414 | try { 415 | await this.#fill(); 416 | } catch (err) { 417 | if (err instanceof PartialReadError) { 418 | err.partial = this.#buf.subarray(this.#r, this.#w); 419 | } else if (err instanceof Error) { 420 | const e = new PartialReadError(); 421 | e.partial = this.#buf.subarray(this.#r, this.#w); 422 | e.stack = err.stack; 423 | e.message = err.message; 424 | e.cause = err.cause; 425 | throw err; 426 | } 427 | throw err; 428 | } 429 | avail = this.#w - this.#r; 430 | } 431 | 432 | if (avail === 0 && this.#eof) { 433 | return null; 434 | } else if (avail < n && this.#eof) { 435 | return this.#buf.subarray(this.#r, this.#r + avail); 436 | } else if (avail < n) { 437 | throw new BufferFullError(this.#buf.subarray(this.#r, this.#w)); 438 | } 439 | 440 | return this.#buf.subarray(this.#r, this.#r + n); 441 | } 442 | } 443 | 444 | abstract class AbstractBufBase { 445 | buf: Uint8Array; 446 | usedBufferBytes = 0; 447 | err: Error | null = null; 448 | 449 | constructor(buf: Uint8Array) { 450 | this.buf = buf; 451 | } 452 | 453 | /** Size returns the size of the underlying buffer in bytes. */ 454 | size(): number { 455 | return this.buf.byteLength; 456 | } 457 | 458 | /** Returns how many bytes are unused in the buffer. */ 459 | available(): number { 460 | return this.buf.byteLength - this.usedBufferBytes; 461 | } 462 | 463 | /** buffered returns the number of bytes that have been written into the 464 | * current buffer. 465 | */ 466 | buffered(): number { 467 | return this.usedBufferBytes; 468 | } 469 | } 470 | 471 | /** BufWriter implements buffering for an deno.Writer object. 472 | * If an error occurs writing to a Writer, no more data will be 473 | * accepted and all subsequent writes, and flush(), will return the error. 474 | * After all data has been written, the client should call the 475 | * flush() method to guarantee all data has been forwarded to 476 | * the underlying deno.Writer. 477 | */ 478 | export class BufWriter extends AbstractBufBase implements Writer { 479 | #writer: Writer; 480 | 481 | /** return new BufWriter unless writer is BufWriter */ 482 | static create(writer: Writer, size: number = DEFAULT_BUF_SIZE): BufWriter { 483 | return writer instanceof BufWriter ? writer : new BufWriter(writer, size); 484 | } 485 | 486 | constructor(writer: Writer, size: number = DEFAULT_BUF_SIZE) { 487 | super(new Uint8Array(size <= 0 ? DEFAULT_BUF_SIZE : size)); 488 | this.#writer = writer; 489 | } 490 | 491 | /** Discards any unflushed buffered data, clears any error, and 492 | * resets buffer to write its output to w. 493 | */ 494 | reset(w: Writer): void { 495 | this.err = null; 496 | this.usedBufferBytes = 0; 497 | this.#writer = w; 498 | } 499 | 500 | /** Flush writes any buffered data to the underlying io.Writer. */ 501 | async flush() { 502 | if (this.err !== null) throw this.err; 503 | if (this.usedBufferBytes === 0) return; 504 | 505 | try { 506 | const p = this.buf.subarray(0, this.usedBufferBytes); 507 | let nwritten = 0; 508 | while (nwritten < p.length) { 509 | nwritten += await this.#writer.write(p.subarray(nwritten)); 510 | } 511 | } catch (e) { 512 | if (e instanceof Error) { 513 | this.err = e; 514 | } 515 | throw e; 516 | } 517 | 518 | this.buf = new Uint8Array(this.buf.length); 519 | this.usedBufferBytes = 0; 520 | } 521 | 522 | /** Writes the contents of `data` into the buffer. If the contents won't fully 523 | * fit into the buffer, those bytes that can are copied into the buffer, the 524 | * buffer is the flushed to the writer and the remaining bytes are copied into 525 | * the now empty buffer. 526 | * 527 | * @return the number of bytes written to the buffer. 528 | */ 529 | async write(data: Uint8Array): Promise { 530 | if (this.err !== null) throw this.err; 531 | if (data.length === 0) return 0; 532 | 533 | let totalBytesWritten = 0; 534 | let numBytesWritten = 0; 535 | while (data.byteLength > this.available()) { 536 | if (this.buffered() === 0) { 537 | // Large write, empty buffer. 538 | // Write directly from data to avoid copy. 539 | try { 540 | numBytesWritten = await this.#writer.write(data); 541 | } catch (e) { 542 | if (e instanceof Error) { 543 | this.err = e; 544 | } 545 | throw e; 546 | } 547 | } else { 548 | numBytesWritten = copy(data, this.buf, this.usedBufferBytes); 549 | this.usedBufferBytes += numBytesWritten; 550 | await this.flush(); 551 | } 552 | totalBytesWritten += numBytesWritten; 553 | data = data.subarray(numBytesWritten); 554 | } 555 | 556 | numBytesWritten = copy(data, this.buf, this.usedBufferBytes); 557 | this.usedBufferBytes += numBytesWritten; 558 | totalBytesWritten += numBytesWritten; 559 | return totalBytesWritten; 560 | } 561 | } 562 | -------------------------------------------------------------------------------- /src/generated/index.ts: -------------------------------------------------------------------------------- 1 | import * as messages from "./messages/index.ts"; 2 | export type { 3 | messages, 4 | }; 5 | -------------------------------------------------------------------------------- /src/generated/messages/index.ts: -------------------------------------------------------------------------------- 1 | import * as pbkit from "./pbkit/index.ts"; 2 | export type { 3 | pbkit, 4 | }; 5 | -------------------------------------------------------------------------------- /src/generated/messages/pbkit/index.ts: -------------------------------------------------------------------------------- 1 | import * as wrp from "./wrp/index.ts"; 2 | export type { 3 | wrp, 4 | }; 5 | -------------------------------------------------------------------------------- /src/generated/messages/pbkit/wrp/WrpGuestMessage_ReqFinish.ts: -------------------------------------------------------------------------------- 1 | import { 2 | tsValueToJsonValueFns, 3 | jsonValueToTsValueFns, 4 | } from "https://deno.land/x/pbkit@v0.0.45/core/runtime/json/scalar.ts"; 5 | import { 6 | WireMessage, 7 | } from "https://deno.land/x/pbkit@v0.0.45/core/runtime/wire/index.ts"; 8 | import { 9 | default as serialize, 10 | } from "https://deno.land/x/pbkit@v0.0.45/core/runtime/wire/serialize.ts"; 11 | import { 12 | tsValueToWireValueFns, 13 | wireValueToTsValueFns, 14 | } from "https://deno.land/x/pbkit@v0.0.45/core/runtime/wire/scalar.ts"; 15 | import { 16 | default as deserialize, 17 | } from "https://deno.land/x/pbkit@v0.0.45/core/runtime/wire/deserialize.ts"; 18 | 19 | export declare namespace $.pbkit.wrp { 20 | export interface WrpGuestMessage_ReqFinish { 21 | reqId: string; 22 | } 23 | } 24 | export type Type = $.pbkit.wrp.WrpGuestMessage_ReqFinish; 25 | 26 | export function getDefaultValue(): $.pbkit.wrp.WrpGuestMessage_ReqFinish { 27 | return { 28 | reqId: "", 29 | }; 30 | } 31 | 32 | export function createValue(partialValue: Partial<$.pbkit.wrp.WrpGuestMessage_ReqFinish>): $.pbkit.wrp.WrpGuestMessage_ReqFinish { 33 | return { 34 | ...getDefaultValue(), 35 | ...partialValue, 36 | }; 37 | } 38 | 39 | export function encodeJson(value: $.pbkit.wrp.WrpGuestMessage_ReqFinish): unknown { 40 | const result: any = {}; 41 | if (value.reqId !== undefined) result.reqId = tsValueToJsonValueFns.string(value.reqId); 42 | return result; 43 | } 44 | 45 | export function decodeJson(value: any): $.pbkit.wrp.WrpGuestMessage_ReqFinish { 46 | const result = getDefaultValue(); 47 | if (value.reqId !== undefined) result.reqId = jsonValueToTsValueFns.string(value.reqId); 48 | return result; 49 | } 50 | 51 | export function encodeBinary(value: $.pbkit.wrp.WrpGuestMessage_ReqFinish): Uint8Array { 52 | const result: WireMessage = []; 53 | if (value.reqId !== undefined) { 54 | const tsValue = value.reqId; 55 | result.push( 56 | [1, tsValueToWireValueFns.string(tsValue)], 57 | ); 58 | } 59 | return serialize(result); 60 | } 61 | 62 | export function decodeBinary(binary: Uint8Array): $.pbkit.wrp.WrpGuestMessage_ReqFinish { 63 | const result = getDefaultValue(); 64 | const wireMessage = deserialize(binary); 65 | const wireFields = new Map(wireMessage); 66 | field: { 67 | const wireValue = wireFields.get(1); 68 | if (wireValue === undefined) break field; 69 | const value = wireValueToTsValueFns.string(wireValue); 70 | if (value === undefined) break field; 71 | result.reqId = value; 72 | } 73 | return result; 74 | } 75 | -------------------------------------------------------------------------------- /src/generated/messages/pbkit/wrp/WrpGuestMessage_ReqPayload.ts: -------------------------------------------------------------------------------- 1 | import { 2 | tsValueToJsonValueFns, 3 | jsonValueToTsValueFns, 4 | } from "https://deno.land/x/pbkit@v0.0.45/core/runtime/json/scalar.ts"; 5 | import { 6 | WireMessage, 7 | } from "https://deno.land/x/pbkit@v0.0.45/core/runtime/wire/index.ts"; 8 | import { 9 | default as serialize, 10 | } from "https://deno.land/x/pbkit@v0.0.45/core/runtime/wire/serialize.ts"; 11 | import { 12 | tsValueToWireValueFns, 13 | wireValueToTsValueFns, 14 | } from "https://deno.land/x/pbkit@v0.0.45/core/runtime/wire/scalar.ts"; 15 | import { 16 | default as deserialize, 17 | } from "https://deno.land/x/pbkit@v0.0.45/core/runtime/wire/deserialize.ts"; 18 | 19 | export declare namespace $.pbkit.wrp { 20 | export interface WrpGuestMessage_ReqPayload { 21 | reqId: string; 22 | payload: Uint8Array; 23 | } 24 | } 25 | export type Type = $.pbkit.wrp.WrpGuestMessage_ReqPayload; 26 | 27 | export function getDefaultValue(): $.pbkit.wrp.WrpGuestMessage_ReqPayload { 28 | return { 29 | reqId: "", 30 | payload: new Uint8Array(), 31 | }; 32 | } 33 | 34 | export function createValue(partialValue: Partial<$.pbkit.wrp.WrpGuestMessage_ReqPayload>): $.pbkit.wrp.WrpGuestMessage_ReqPayload { 35 | return { 36 | ...getDefaultValue(), 37 | ...partialValue, 38 | }; 39 | } 40 | 41 | export function encodeJson(value: $.pbkit.wrp.WrpGuestMessage_ReqPayload): unknown { 42 | const result: any = {}; 43 | if (value.reqId !== undefined) result.reqId = tsValueToJsonValueFns.string(value.reqId); 44 | if (value.payload !== undefined) result.payload = tsValueToJsonValueFns.bytes(value.payload); 45 | return result; 46 | } 47 | 48 | export function decodeJson(value: any): $.pbkit.wrp.WrpGuestMessage_ReqPayload { 49 | const result = getDefaultValue(); 50 | if (value.reqId !== undefined) result.reqId = jsonValueToTsValueFns.string(value.reqId); 51 | if (value.payload !== undefined) result.payload = jsonValueToTsValueFns.bytes(value.payload); 52 | return result; 53 | } 54 | 55 | export function encodeBinary(value: $.pbkit.wrp.WrpGuestMessage_ReqPayload): Uint8Array { 56 | const result: WireMessage = []; 57 | if (value.reqId !== undefined) { 58 | const tsValue = value.reqId; 59 | result.push( 60 | [1, tsValueToWireValueFns.string(tsValue)], 61 | ); 62 | } 63 | if (value.payload !== undefined) { 64 | const tsValue = value.payload; 65 | result.push( 66 | [2, tsValueToWireValueFns.bytes(tsValue)], 67 | ); 68 | } 69 | return serialize(result); 70 | } 71 | 72 | export function decodeBinary(binary: Uint8Array): $.pbkit.wrp.WrpGuestMessage_ReqPayload { 73 | const result = getDefaultValue(); 74 | const wireMessage = deserialize(binary); 75 | const wireFields = new Map(wireMessage); 76 | field: { 77 | const wireValue = wireFields.get(1); 78 | if (wireValue === undefined) break field; 79 | const value = wireValueToTsValueFns.string(wireValue); 80 | if (value === undefined) break field; 81 | result.reqId = value; 82 | } 83 | field: { 84 | const wireValue = wireFields.get(2); 85 | if (wireValue === undefined) break field; 86 | const value = wireValueToTsValueFns.bytes(wireValue); 87 | if (value === undefined) break field; 88 | result.payload = value; 89 | } 90 | return result; 91 | } 92 | -------------------------------------------------------------------------------- /src/generated/messages/pbkit/wrp/WrpGuestMessage_ReqStart.ts: -------------------------------------------------------------------------------- 1 | import { 2 | tsValueToJsonValueFns, 3 | jsonValueToTsValueFns, 4 | } from "https://deno.land/x/pbkit@v0.0.45/core/runtime/json/scalar.ts"; 5 | import { 6 | WireMessage, 7 | WireType, 8 | } from "https://deno.land/x/pbkit@v0.0.45/core/runtime/wire/index.ts"; 9 | import { 10 | default as serialize, 11 | } from "https://deno.land/x/pbkit@v0.0.45/core/runtime/wire/serialize.ts"; 12 | import { 13 | tsValueToWireValueFns, 14 | wireValueToTsValueFns, 15 | } from "https://deno.land/x/pbkit@v0.0.45/core/runtime/wire/scalar.ts"; 16 | import { 17 | default as deserialize, 18 | } from "https://deno.land/x/pbkit@v0.0.45/core/runtime/wire/deserialize.ts"; 19 | 20 | export declare namespace $.pbkit.wrp { 21 | export interface WrpGuestMessage_ReqStart { 22 | reqId: string; 23 | methodName: string; 24 | metadata: Map; 25 | } 26 | } 27 | export type Type = $.pbkit.wrp.WrpGuestMessage_ReqStart; 28 | 29 | export function getDefaultValue(): $.pbkit.wrp.WrpGuestMessage_ReqStart { 30 | return { 31 | reqId: "", 32 | methodName: "", 33 | metadata: new Map(), 34 | }; 35 | } 36 | 37 | export function createValue(partialValue: Partial<$.pbkit.wrp.WrpGuestMessage_ReqStart>): $.pbkit.wrp.WrpGuestMessage_ReqStart { 38 | return { 39 | ...getDefaultValue(), 40 | ...partialValue, 41 | }; 42 | } 43 | 44 | export function encodeJson(value: $.pbkit.wrp.WrpGuestMessage_ReqStart): unknown { 45 | const result: any = {}; 46 | if (value.reqId !== undefined) result.reqId = tsValueToJsonValueFns.string(value.reqId); 47 | if (value.methodName !== undefined) result.methodName = tsValueToJsonValueFns.string(value.methodName); 48 | if (value.metadata !== undefined) result.metadata = Object.fromEntries([...value.metadata.entries()].map(([key, value]) => [key, tsValueToJsonValueFns.string(value)])); 49 | return result; 50 | } 51 | 52 | export function decodeJson(value: any): $.pbkit.wrp.WrpGuestMessage_ReqStart { 53 | const result = getDefaultValue(); 54 | if (value.reqId !== undefined) result.reqId = jsonValueToTsValueFns.string(value.reqId); 55 | if (value.methodName !== undefined) result.methodName = jsonValueToTsValueFns.string(value.methodName); 56 | if (value.metadata !== undefined) result.metadata = Object.fromEntries([...value.metadata.entries()].map(([key, value]) => [key, jsonValueToTsValueFns.string(value)])); 57 | return result; 58 | } 59 | 60 | export function encodeBinary(value: $.pbkit.wrp.WrpGuestMessage_ReqStart): Uint8Array { 61 | const result: WireMessage = []; 62 | if (value.reqId !== undefined) { 63 | const tsValue = value.reqId; 64 | result.push( 65 | [1, tsValueToWireValueFns.string(tsValue)], 66 | ); 67 | } 68 | if (value.methodName !== undefined) { 69 | const tsValue = value.methodName; 70 | result.push( 71 | [2, tsValueToWireValueFns.string(tsValue)], 72 | ); 73 | } 74 | { 75 | const fields = value.metadata.entries(); 76 | for (const [key, value] of fields) { 77 | result.push( 78 | [3, { type: WireType.LengthDelimited as const, value: serialize([[1, tsValueToWireValueFns.string(key)], [2, tsValueToWireValueFns.string(value)]]) }], 79 | ); 80 | } 81 | } 82 | return serialize(result); 83 | } 84 | 85 | export function decodeBinary(binary: Uint8Array): $.pbkit.wrp.WrpGuestMessage_ReqStart { 86 | const result = getDefaultValue(); 87 | const wireMessage = deserialize(binary); 88 | const wireFields = new Map(wireMessage); 89 | field: { 90 | const wireValue = wireFields.get(1); 91 | if (wireValue === undefined) break field; 92 | const value = wireValueToTsValueFns.string(wireValue); 93 | if (value === undefined) break field; 94 | result.reqId = value; 95 | } 96 | field: { 97 | const wireValue = wireFields.get(2); 98 | if (wireValue === undefined) break field; 99 | const value = wireValueToTsValueFns.string(wireValue); 100 | if (value === undefined) break field; 101 | result.methodName = value; 102 | } 103 | collection: { 104 | const wireValues = wireMessage.filter(([fieldNumber]) => fieldNumber === 3).map(([, wireValue]) => wireValue); 105 | const value = wireValues.map((wireValue) => (() => { if (wireValue.type !== WireType.LengthDelimited) { return; } const { 1: key, 2: value } = Object.fromEntries(deserialize(wireValue.value)); if (key === undefined || value === undefined) return; return [wireValueToTsValueFns.string(key), wireValueToTsValueFns.string(value)] as const;})()).filter(x => x !== undefined); 106 | if (!value.length) break collection; 107 | result.metadata = new Map(value as any); 108 | } 109 | return result; 110 | } 111 | -------------------------------------------------------------------------------- /src/generated/messages/pbkit/wrp/WrpGuestMessage_ResFinish.ts: -------------------------------------------------------------------------------- 1 | import { 2 | tsValueToJsonValueFns, 3 | jsonValueToTsValueFns, 4 | } from "https://deno.land/x/pbkit@v0.0.45/core/runtime/json/scalar.ts"; 5 | import { 6 | WireMessage, 7 | } from "https://deno.land/x/pbkit@v0.0.45/core/runtime/wire/index.ts"; 8 | import { 9 | default as serialize, 10 | } from "https://deno.land/x/pbkit@v0.0.45/core/runtime/wire/serialize.ts"; 11 | import { 12 | tsValueToWireValueFns, 13 | wireValueToTsValueFns, 14 | } from "https://deno.land/x/pbkit@v0.0.45/core/runtime/wire/scalar.ts"; 15 | import { 16 | default as deserialize, 17 | } from "https://deno.land/x/pbkit@v0.0.45/core/runtime/wire/deserialize.ts"; 18 | 19 | export declare namespace $.pbkit.wrp { 20 | export interface WrpGuestMessage_ResFinish { 21 | reqId: string; 22 | } 23 | } 24 | export type Type = $.pbkit.wrp.WrpGuestMessage_ResFinish; 25 | 26 | export function getDefaultValue(): $.pbkit.wrp.WrpGuestMessage_ResFinish { 27 | return { 28 | reqId: "", 29 | }; 30 | } 31 | 32 | export function createValue(partialValue: Partial<$.pbkit.wrp.WrpGuestMessage_ResFinish>): $.pbkit.wrp.WrpGuestMessage_ResFinish { 33 | return { 34 | ...getDefaultValue(), 35 | ...partialValue, 36 | }; 37 | } 38 | 39 | export function encodeJson(value: $.pbkit.wrp.WrpGuestMessage_ResFinish): unknown { 40 | const result: any = {}; 41 | if (value.reqId !== undefined) result.reqId = tsValueToJsonValueFns.string(value.reqId); 42 | return result; 43 | } 44 | 45 | export function decodeJson(value: any): $.pbkit.wrp.WrpGuestMessage_ResFinish { 46 | const result = getDefaultValue(); 47 | if (value.reqId !== undefined) result.reqId = jsonValueToTsValueFns.string(value.reqId); 48 | return result; 49 | } 50 | 51 | export function encodeBinary(value: $.pbkit.wrp.WrpGuestMessage_ResFinish): Uint8Array { 52 | const result: WireMessage = []; 53 | if (value.reqId !== undefined) { 54 | const tsValue = value.reqId; 55 | result.push( 56 | [1, tsValueToWireValueFns.string(tsValue)], 57 | ); 58 | } 59 | return serialize(result); 60 | } 61 | 62 | export function decodeBinary(binary: Uint8Array): $.pbkit.wrp.WrpGuestMessage_ResFinish { 63 | const result = getDefaultValue(); 64 | const wireMessage = deserialize(binary); 65 | const wireFields = new Map(wireMessage); 66 | field: { 67 | const wireValue = wireFields.get(1); 68 | if (wireValue === undefined) break field; 69 | const value = wireValueToTsValueFns.string(wireValue); 70 | if (value === undefined) break field; 71 | result.reqId = value; 72 | } 73 | return result; 74 | } 75 | -------------------------------------------------------------------------------- /src/generated/messages/pbkit/wrp/WrpHostMessage_Error.ts: -------------------------------------------------------------------------------- 1 | import { 2 | tsValueToJsonValueFns, 3 | jsonValueToTsValueFns, 4 | } from "https://deno.land/x/pbkit@v0.0.45/core/runtime/json/scalar.ts"; 5 | import { 6 | WireMessage, 7 | } from "https://deno.land/x/pbkit@v0.0.45/core/runtime/wire/index.ts"; 8 | import { 9 | default as serialize, 10 | } from "https://deno.land/x/pbkit@v0.0.45/core/runtime/wire/serialize.ts"; 11 | import { 12 | tsValueToWireValueFns, 13 | wireValueToTsValueFns, 14 | } from "https://deno.land/x/pbkit@v0.0.45/core/runtime/wire/scalar.ts"; 15 | import { 16 | default as deserialize, 17 | } from "https://deno.land/x/pbkit@v0.0.45/core/runtime/wire/deserialize.ts"; 18 | 19 | export declare namespace $.pbkit.wrp { 20 | export interface WrpHostMessage_Error { 21 | message: string; 22 | } 23 | } 24 | export type Type = $.pbkit.wrp.WrpHostMessage_Error; 25 | 26 | export function getDefaultValue(): $.pbkit.wrp.WrpHostMessage_Error { 27 | return { 28 | message: "", 29 | }; 30 | } 31 | 32 | export function createValue(partialValue: Partial<$.pbkit.wrp.WrpHostMessage_Error>): $.pbkit.wrp.WrpHostMessage_Error { 33 | return { 34 | ...getDefaultValue(), 35 | ...partialValue, 36 | }; 37 | } 38 | 39 | export function encodeJson(value: $.pbkit.wrp.WrpHostMessage_Error): unknown { 40 | const result: any = {}; 41 | if (value.message !== undefined) result.message = tsValueToJsonValueFns.string(value.message); 42 | return result; 43 | } 44 | 45 | export function decodeJson(value: any): $.pbkit.wrp.WrpHostMessage_Error { 46 | const result = getDefaultValue(); 47 | if (value.message !== undefined) result.message = jsonValueToTsValueFns.string(value.message); 48 | return result; 49 | } 50 | 51 | export function encodeBinary(value: $.pbkit.wrp.WrpHostMessage_Error): Uint8Array { 52 | const result: WireMessage = []; 53 | if (value.message !== undefined) { 54 | const tsValue = value.message; 55 | result.push( 56 | [1, tsValueToWireValueFns.string(tsValue)], 57 | ); 58 | } 59 | return serialize(result); 60 | } 61 | 62 | export function decodeBinary(binary: Uint8Array): $.pbkit.wrp.WrpHostMessage_Error { 63 | const result = getDefaultValue(); 64 | const wireMessage = deserialize(binary); 65 | const wireFields = new Map(wireMessage); 66 | field: { 67 | const wireValue = wireFields.get(1); 68 | if (wireValue === undefined) break field; 69 | const value = wireValueToTsValueFns.string(wireValue); 70 | if (value === undefined) break field; 71 | result.message = value; 72 | } 73 | return result; 74 | } 75 | -------------------------------------------------------------------------------- /src/generated/messages/pbkit/wrp/WrpHostMessage_Initialize.ts: -------------------------------------------------------------------------------- 1 | import { 2 | tsValueToJsonValueFns, 3 | jsonValueToTsValueFns, 4 | } from "https://deno.land/x/pbkit@v0.0.45/core/runtime/json/scalar.ts"; 5 | import { 6 | WireMessage, 7 | } from "https://deno.land/x/pbkit@v0.0.45/core/runtime/wire/index.ts"; 8 | import { 9 | default as serialize, 10 | } from "https://deno.land/x/pbkit@v0.0.45/core/runtime/wire/serialize.ts"; 11 | import { 12 | tsValueToWireValueFns, 13 | wireValueToTsValueFns, 14 | } from "https://deno.land/x/pbkit@v0.0.45/core/runtime/wire/scalar.ts"; 15 | import { 16 | default as deserialize, 17 | } from "https://deno.land/x/pbkit@v0.0.45/core/runtime/wire/deserialize.ts"; 18 | 19 | export declare namespace $.pbkit.wrp { 20 | export interface WrpHostMessage_Initialize { 21 | availableMethods: string[]; 22 | } 23 | } 24 | export type Type = $.pbkit.wrp.WrpHostMessage_Initialize; 25 | 26 | export function getDefaultValue(): $.pbkit.wrp.WrpHostMessage_Initialize { 27 | return { 28 | availableMethods: [], 29 | }; 30 | } 31 | 32 | export function createValue(partialValue: Partial<$.pbkit.wrp.WrpHostMessage_Initialize>): $.pbkit.wrp.WrpHostMessage_Initialize { 33 | return { 34 | ...getDefaultValue(), 35 | ...partialValue, 36 | }; 37 | } 38 | 39 | export function encodeJson(value: $.pbkit.wrp.WrpHostMessage_Initialize): unknown { 40 | const result: any = {}; 41 | result.availableMethods = value.availableMethods.map(value => tsValueToJsonValueFns.string(value)); 42 | return result; 43 | } 44 | 45 | export function decodeJson(value: any): $.pbkit.wrp.WrpHostMessage_Initialize { 46 | const result = getDefaultValue(); 47 | result.availableMethods = value.availableMethods?.map((value: any) => jsonValueToTsValueFns.string(value)) ?? []; 48 | return result; 49 | } 50 | 51 | export function encodeBinary(value: $.pbkit.wrp.WrpHostMessage_Initialize): Uint8Array { 52 | const result: WireMessage = []; 53 | for (const tsValue of value.availableMethods) { 54 | result.push( 55 | [1, tsValueToWireValueFns.string(tsValue)], 56 | ); 57 | } 58 | return serialize(result); 59 | } 60 | 61 | export function decodeBinary(binary: Uint8Array): $.pbkit.wrp.WrpHostMessage_Initialize { 62 | const result = getDefaultValue(); 63 | const wireMessage = deserialize(binary); 64 | const wireFields = new Map(wireMessage); 65 | collection: { 66 | const wireValues = wireMessage.filter(([fieldNumber]) => fieldNumber === 1).map(([, wireValue]) => wireValue); 67 | const value = wireValues.map((wireValue) => wireValueToTsValueFns.string(wireValue)).filter(x => x !== undefined); 68 | if (!value.length) break collection; 69 | result.availableMethods = value as any; 70 | } 71 | return result; 72 | } 73 | -------------------------------------------------------------------------------- /src/generated/messages/pbkit/wrp/WrpHostMessage_ResFinish.ts: -------------------------------------------------------------------------------- 1 | import { 2 | tsValueToJsonValueFns, 3 | jsonValueToTsValueFns, 4 | } from "https://deno.land/x/pbkit@v0.0.45/core/runtime/json/scalar.ts"; 5 | import { 6 | WireMessage, 7 | WireType, 8 | } from "https://deno.land/x/pbkit@v0.0.45/core/runtime/wire/index.ts"; 9 | import { 10 | default as serialize, 11 | } from "https://deno.land/x/pbkit@v0.0.45/core/runtime/wire/serialize.ts"; 12 | import { 13 | tsValueToWireValueFns, 14 | wireValueToTsValueFns, 15 | } from "https://deno.land/x/pbkit@v0.0.45/core/runtime/wire/scalar.ts"; 16 | import { 17 | default as deserialize, 18 | } from "https://deno.land/x/pbkit@v0.0.45/core/runtime/wire/deserialize.ts"; 19 | 20 | export declare namespace $.pbkit.wrp { 21 | export interface WrpHostMessage_ResFinish { 22 | reqId: string; 23 | trailer: Map; 24 | } 25 | } 26 | export type Type = $.pbkit.wrp.WrpHostMessage_ResFinish; 27 | 28 | export function getDefaultValue(): $.pbkit.wrp.WrpHostMessage_ResFinish { 29 | return { 30 | reqId: "", 31 | trailer: new Map(), 32 | }; 33 | } 34 | 35 | export function createValue(partialValue: Partial<$.pbkit.wrp.WrpHostMessage_ResFinish>): $.pbkit.wrp.WrpHostMessage_ResFinish { 36 | return { 37 | ...getDefaultValue(), 38 | ...partialValue, 39 | }; 40 | } 41 | 42 | export function encodeJson(value: $.pbkit.wrp.WrpHostMessage_ResFinish): unknown { 43 | const result: any = {}; 44 | if (value.reqId !== undefined) result.reqId = tsValueToJsonValueFns.string(value.reqId); 45 | if (value.trailer !== undefined) result.trailer = Object.fromEntries([...value.trailer.entries()].map(([key, value]) => [key, tsValueToJsonValueFns.string(value)])); 46 | return result; 47 | } 48 | 49 | export function decodeJson(value: any): $.pbkit.wrp.WrpHostMessage_ResFinish { 50 | const result = getDefaultValue(); 51 | if (value.reqId !== undefined) result.reqId = jsonValueToTsValueFns.string(value.reqId); 52 | if (value.trailer !== undefined) result.trailer = Object.fromEntries([...value.trailer.entries()].map(([key, value]) => [key, jsonValueToTsValueFns.string(value)])); 53 | return result; 54 | } 55 | 56 | export function encodeBinary(value: $.pbkit.wrp.WrpHostMessage_ResFinish): Uint8Array { 57 | const result: WireMessage = []; 58 | if (value.reqId !== undefined) { 59 | const tsValue = value.reqId; 60 | result.push( 61 | [1, tsValueToWireValueFns.string(tsValue)], 62 | ); 63 | } 64 | { 65 | const fields = value.trailer.entries(); 66 | for (const [key, value] of fields) { 67 | result.push( 68 | [2, { type: WireType.LengthDelimited as const, value: serialize([[1, tsValueToWireValueFns.string(key)], [2, tsValueToWireValueFns.string(value)]]) }], 69 | ); 70 | } 71 | } 72 | return serialize(result); 73 | } 74 | 75 | export function decodeBinary(binary: Uint8Array): $.pbkit.wrp.WrpHostMessage_ResFinish { 76 | const result = getDefaultValue(); 77 | const wireMessage = deserialize(binary); 78 | const wireFields = new Map(wireMessage); 79 | field: { 80 | const wireValue = wireFields.get(1); 81 | if (wireValue === undefined) break field; 82 | const value = wireValueToTsValueFns.string(wireValue); 83 | if (value === undefined) break field; 84 | result.reqId = value; 85 | } 86 | collection: { 87 | const wireValues = wireMessage.filter(([fieldNumber]) => fieldNumber === 2).map(([, wireValue]) => wireValue); 88 | const value = wireValues.map((wireValue) => (() => { if (wireValue.type !== WireType.LengthDelimited) { return; } const { 1: key, 2: value } = Object.fromEntries(deserialize(wireValue.value)); if (key === undefined || value === undefined) return; return [wireValueToTsValueFns.string(key), wireValueToTsValueFns.string(value)] as const;})()).filter(x => x !== undefined); 89 | if (!value.length) break collection; 90 | result.trailer = new Map(value as any); 91 | } 92 | return result; 93 | } 94 | -------------------------------------------------------------------------------- /src/generated/messages/pbkit/wrp/WrpHostMessage_ResPayload.ts: -------------------------------------------------------------------------------- 1 | import { 2 | tsValueToJsonValueFns, 3 | jsonValueToTsValueFns, 4 | } from "https://deno.land/x/pbkit@v0.0.45/core/runtime/json/scalar.ts"; 5 | import { 6 | WireMessage, 7 | } from "https://deno.land/x/pbkit@v0.0.45/core/runtime/wire/index.ts"; 8 | import { 9 | default as serialize, 10 | } from "https://deno.land/x/pbkit@v0.0.45/core/runtime/wire/serialize.ts"; 11 | import { 12 | tsValueToWireValueFns, 13 | wireValueToTsValueFns, 14 | } from "https://deno.land/x/pbkit@v0.0.45/core/runtime/wire/scalar.ts"; 15 | import { 16 | default as deserialize, 17 | } from "https://deno.land/x/pbkit@v0.0.45/core/runtime/wire/deserialize.ts"; 18 | 19 | export declare namespace $.pbkit.wrp { 20 | export interface WrpHostMessage_ResPayload { 21 | reqId: string; 22 | payload: Uint8Array; 23 | } 24 | } 25 | export type Type = $.pbkit.wrp.WrpHostMessage_ResPayload; 26 | 27 | export function getDefaultValue(): $.pbkit.wrp.WrpHostMessage_ResPayload { 28 | return { 29 | reqId: "", 30 | payload: new Uint8Array(), 31 | }; 32 | } 33 | 34 | export function createValue(partialValue: Partial<$.pbkit.wrp.WrpHostMessage_ResPayload>): $.pbkit.wrp.WrpHostMessage_ResPayload { 35 | return { 36 | ...getDefaultValue(), 37 | ...partialValue, 38 | }; 39 | } 40 | 41 | export function encodeJson(value: $.pbkit.wrp.WrpHostMessage_ResPayload): unknown { 42 | const result: any = {}; 43 | if (value.reqId !== undefined) result.reqId = tsValueToJsonValueFns.string(value.reqId); 44 | if (value.payload !== undefined) result.payload = tsValueToJsonValueFns.bytes(value.payload); 45 | return result; 46 | } 47 | 48 | export function decodeJson(value: any): $.pbkit.wrp.WrpHostMessage_ResPayload { 49 | const result = getDefaultValue(); 50 | if (value.reqId !== undefined) result.reqId = jsonValueToTsValueFns.string(value.reqId); 51 | if (value.payload !== undefined) result.payload = jsonValueToTsValueFns.bytes(value.payload); 52 | return result; 53 | } 54 | 55 | export function encodeBinary(value: $.pbkit.wrp.WrpHostMessage_ResPayload): Uint8Array { 56 | const result: WireMessage = []; 57 | if (value.reqId !== undefined) { 58 | const tsValue = value.reqId; 59 | result.push( 60 | [1, tsValueToWireValueFns.string(tsValue)], 61 | ); 62 | } 63 | if (value.payload !== undefined) { 64 | const tsValue = value.payload; 65 | result.push( 66 | [2, tsValueToWireValueFns.bytes(tsValue)], 67 | ); 68 | } 69 | return serialize(result); 70 | } 71 | 72 | export function decodeBinary(binary: Uint8Array): $.pbkit.wrp.WrpHostMessage_ResPayload { 73 | const result = getDefaultValue(); 74 | const wireMessage = deserialize(binary); 75 | const wireFields = new Map(wireMessage); 76 | field: { 77 | const wireValue = wireFields.get(1); 78 | if (wireValue === undefined) break field; 79 | const value = wireValueToTsValueFns.string(wireValue); 80 | if (value === undefined) break field; 81 | result.reqId = value; 82 | } 83 | field: { 84 | const wireValue = wireFields.get(2); 85 | if (wireValue === undefined) break field; 86 | const value = wireValueToTsValueFns.bytes(wireValue); 87 | if (value === undefined) break field; 88 | result.payload = value; 89 | } 90 | return result; 91 | } 92 | -------------------------------------------------------------------------------- /src/generated/messages/pbkit/wrp/WrpHostMessage_ResStart.ts: -------------------------------------------------------------------------------- 1 | import { 2 | tsValueToJsonValueFns, 3 | jsonValueToTsValueFns, 4 | } from "https://deno.land/x/pbkit@v0.0.45/core/runtime/json/scalar.ts"; 5 | import { 6 | WireMessage, 7 | WireType, 8 | } from "https://deno.land/x/pbkit@v0.0.45/core/runtime/wire/index.ts"; 9 | import { 10 | default as serialize, 11 | } from "https://deno.land/x/pbkit@v0.0.45/core/runtime/wire/serialize.ts"; 12 | import { 13 | tsValueToWireValueFns, 14 | wireValueToTsValueFns, 15 | } from "https://deno.land/x/pbkit@v0.0.45/core/runtime/wire/scalar.ts"; 16 | import { 17 | default as deserialize, 18 | } from "https://deno.land/x/pbkit@v0.0.45/core/runtime/wire/deserialize.ts"; 19 | 20 | export declare namespace $.pbkit.wrp { 21 | export interface WrpHostMessage_ResStart { 22 | reqId: string; 23 | header: Map; 24 | } 25 | } 26 | export type Type = $.pbkit.wrp.WrpHostMessage_ResStart; 27 | 28 | export function getDefaultValue(): $.pbkit.wrp.WrpHostMessage_ResStart { 29 | return { 30 | reqId: "", 31 | header: new Map(), 32 | }; 33 | } 34 | 35 | export function createValue(partialValue: Partial<$.pbkit.wrp.WrpHostMessage_ResStart>): $.pbkit.wrp.WrpHostMessage_ResStart { 36 | return { 37 | ...getDefaultValue(), 38 | ...partialValue, 39 | }; 40 | } 41 | 42 | export function encodeJson(value: $.pbkit.wrp.WrpHostMessage_ResStart): unknown { 43 | const result: any = {}; 44 | if (value.reqId !== undefined) result.reqId = tsValueToJsonValueFns.string(value.reqId); 45 | if (value.header !== undefined) result.header = Object.fromEntries([...value.header.entries()].map(([key, value]) => [key, tsValueToJsonValueFns.string(value)])); 46 | return result; 47 | } 48 | 49 | export function decodeJson(value: any): $.pbkit.wrp.WrpHostMessage_ResStart { 50 | const result = getDefaultValue(); 51 | if (value.reqId !== undefined) result.reqId = jsonValueToTsValueFns.string(value.reqId); 52 | if (value.header !== undefined) result.header = Object.fromEntries([...value.header.entries()].map(([key, value]) => [key, jsonValueToTsValueFns.string(value)])); 53 | return result; 54 | } 55 | 56 | export function encodeBinary(value: $.pbkit.wrp.WrpHostMessage_ResStart): Uint8Array { 57 | const result: WireMessage = []; 58 | if (value.reqId !== undefined) { 59 | const tsValue = value.reqId; 60 | result.push( 61 | [1, tsValueToWireValueFns.string(tsValue)], 62 | ); 63 | } 64 | { 65 | const fields = value.header.entries(); 66 | for (const [key, value] of fields) { 67 | result.push( 68 | [2, { type: WireType.LengthDelimited as const, value: serialize([[1, tsValueToWireValueFns.string(key)], [2, tsValueToWireValueFns.string(value)]]) }], 69 | ); 70 | } 71 | } 72 | return serialize(result); 73 | } 74 | 75 | export function decodeBinary(binary: Uint8Array): $.pbkit.wrp.WrpHostMessage_ResStart { 76 | const result = getDefaultValue(); 77 | const wireMessage = deserialize(binary); 78 | const wireFields = new Map(wireMessage); 79 | field: { 80 | const wireValue = wireFields.get(1); 81 | if (wireValue === undefined) break field; 82 | const value = wireValueToTsValueFns.string(wireValue); 83 | if (value === undefined) break field; 84 | result.reqId = value; 85 | } 86 | collection: { 87 | const wireValues = wireMessage.filter(([fieldNumber]) => fieldNumber === 2).map(([, wireValue]) => wireValue); 88 | const value = wireValues.map((wireValue) => (() => { if (wireValue.type !== WireType.LengthDelimited) { return; } const { 1: key, 2: value } = Object.fromEntries(deserialize(wireValue.value)); if (key === undefined || value === undefined) return; return [wireValueToTsValueFns.string(key), wireValueToTsValueFns.string(value)] as const;})()).filter(x => x !== undefined); 89 | if (!value.length) break collection; 90 | result.header = new Map(value as any); 91 | } 92 | return result; 93 | } 94 | -------------------------------------------------------------------------------- /src/generated/messages/pbkit/wrp/WrpMessage.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Type as WrpHostMessage_Initialize, 3 | encodeJson as encodeJson_1, 4 | decodeJson as decodeJson_1, 5 | encodeBinary as encodeBinary_1, 6 | decodeBinary as decodeBinary_1, 7 | } from "./WrpHostMessage_Initialize.ts"; 8 | import { 9 | Type as WrpHostMessage_Error, 10 | encodeJson as encodeJson_2, 11 | decodeJson as decodeJson_2, 12 | encodeBinary as encodeBinary_2, 13 | decodeBinary as decodeBinary_2, 14 | } from "./WrpHostMessage_Error.ts"; 15 | import { 16 | Type as WrpHostMessage_ResStart, 17 | encodeJson as encodeJson_3, 18 | decodeJson as decodeJson_3, 19 | encodeBinary as encodeBinary_3, 20 | decodeBinary as decodeBinary_3, 21 | } from "./WrpHostMessage_ResStart.ts"; 22 | import { 23 | Type as WrpHostMessage_ResPayload, 24 | encodeJson as encodeJson_4, 25 | decodeJson as decodeJson_4, 26 | encodeBinary as encodeBinary_4, 27 | decodeBinary as decodeBinary_4, 28 | } from "./WrpHostMessage_ResPayload.ts"; 29 | import { 30 | Type as WrpHostMessage_ResFinish, 31 | encodeJson as encodeJson_5, 32 | decodeJson as decodeJson_5, 33 | encodeBinary as encodeBinary_5, 34 | decodeBinary as decodeBinary_5, 35 | } from "./WrpHostMessage_ResFinish.ts"; 36 | import { 37 | Type as WrpGuestMessage_ReqStart, 38 | encodeJson as encodeJson_6, 39 | decodeJson as decodeJson_6, 40 | encodeBinary as encodeBinary_6, 41 | decodeBinary as decodeBinary_6, 42 | } from "./WrpGuestMessage_ReqStart.ts"; 43 | import { 44 | Type as WrpGuestMessage_ReqPayload, 45 | encodeJson as encodeJson_7, 46 | decodeJson as decodeJson_7, 47 | encodeBinary as encodeBinary_7, 48 | decodeBinary as decodeBinary_7, 49 | } from "./WrpGuestMessage_ReqPayload.ts"; 50 | import { 51 | Type as WrpGuestMessage_ReqFinish, 52 | encodeJson as encodeJson_8, 53 | decodeJson as decodeJson_8, 54 | encodeBinary as encodeBinary_8, 55 | decodeBinary as decodeBinary_8, 56 | } from "./WrpGuestMessage_ReqFinish.ts"; 57 | import { 58 | Type as WrpGuestMessage_ResFinish, 59 | encodeJson as encodeJson_9, 60 | decodeJson as decodeJson_9, 61 | encodeBinary as encodeBinary_9, 62 | decodeBinary as decodeBinary_9, 63 | } from "./WrpGuestMessage_ResFinish.ts"; 64 | import { 65 | jsonValueToTsValueFns, 66 | } from "https://deno.land/x/pbkit@v0.0.45/core/runtime/json/scalar.ts"; 67 | import { 68 | WireMessage, 69 | WireType, 70 | Field, 71 | } from "https://deno.land/x/pbkit@v0.0.45/core/runtime/wire/index.ts"; 72 | import { 73 | default as serialize, 74 | } from "https://deno.land/x/pbkit@v0.0.45/core/runtime/wire/serialize.ts"; 75 | import { 76 | default as deserialize, 77 | } from "https://deno.land/x/pbkit@v0.0.45/core/runtime/wire/deserialize.ts"; 78 | 79 | export declare namespace $.pbkit.wrp { 80 | export interface WrpMessage { 81 | message?: ( 82 | | { field: "HostInitialize", value: WrpHostMessage_Initialize } 83 | | { field: "HostError", value: WrpHostMessage_Error } 84 | | { field: "HostResStart", value: WrpHostMessage_ResStart } 85 | | { field: "HostResPayload", value: WrpHostMessage_ResPayload } 86 | | { field: "HostResFinish", value: WrpHostMessage_ResFinish } 87 | | { field: "GuestReqStart", value: WrpGuestMessage_ReqStart } 88 | | { field: "GuestReqPayload", value: WrpGuestMessage_ReqPayload } 89 | | { field: "GuestReqFinish", value: WrpGuestMessage_ReqFinish } 90 | | { field: "GuestResFinish", value: WrpGuestMessage_ResFinish } 91 | ); 92 | } 93 | } 94 | export type Type = $.pbkit.wrp.WrpMessage; 95 | 96 | export function getDefaultValue(): $.pbkit.wrp.WrpMessage { 97 | return { 98 | message: undefined, 99 | }; 100 | } 101 | 102 | export function createValue(partialValue: Partial<$.pbkit.wrp.WrpMessage>): $.pbkit.wrp.WrpMessage { 103 | return { 104 | ...getDefaultValue(), 105 | ...partialValue, 106 | }; 107 | } 108 | 109 | export function encodeJson(value: $.pbkit.wrp.WrpMessage): unknown { 110 | const result: any = {}; 111 | switch (value.message?.field) { 112 | case "HostInitialize": { 113 | result.HostInitialize = encodeJson_1(value.message.value); 114 | break; 115 | } 116 | case "HostError": { 117 | result.HostError = encodeJson_2(value.message.value); 118 | break; 119 | } 120 | case "HostResStart": { 121 | result.HostResStart = encodeJson_3(value.message.value); 122 | break; 123 | } 124 | case "HostResPayload": { 125 | result.HostResPayload = encodeJson_4(value.message.value); 126 | break; 127 | } 128 | case "HostResFinish": { 129 | result.HostResFinish = encodeJson_5(value.message.value); 130 | break; 131 | } 132 | case "GuestReqStart": { 133 | result.GuestReqStart = encodeJson_6(value.message.value); 134 | break; 135 | } 136 | case "GuestReqPayload": { 137 | result.GuestReqPayload = encodeJson_7(value.message.value); 138 | break; 139 | } 140 | case "GuestReqFinish": { 141 | result.GuestReqFinish = encodeJson_8(value.message.value); 142 | break; 143 | } 144 | case "GuestResFinish": { 145 | result.GuestResFinish = encodeJson_9(value.message.value); 146 | break; 147 | } 148 | } 149 | return result; 150 | } 151 | 152 | export function decodeJson(value: any): $.pbkit.wrp.WrpMessage { 153 | const result = getDefaultValue(); 154 | if (value.HostInitialize !== undefined) result.message = {field: "HostInitialize", value: decodeJson_1(value.HostInitialize)}; 155 | if (value.HostError !== undefined) result.message = {field: "HostError", value: decodeJson_2(value.HostError)}; 156 | if (value.HostResStart !== undefined) result.message = {field: "HostResStart", value: decodeJson_3(value.HostResStart)}; 157 | if (value.HostResPayload !== undefined) result.message = {field: "HostResPayload", value: decodeJson_4(value.HostResPayload)}; 158 | if (value.HostResFinish !== undefined) result.message = {field: "HostResFinish", value: decodeJson_5(value.HostResFinish)}; 159 | if (value.GuestReqStart !== undefined) result.message = {field: "GuestReqStart", value: decodeJson_6(value.GuestReqStart)}; 160 | if (value.GuestReqPayload !== undefined) result.message = {field: "GuestReqPayload", value: decodeJson_7(value.GuestReqPayload)}; 161 | if (value.GuestReqFinish !== undefined) result.message = {field: "GuestReqFinish", value: decodeJson_8(value.GuestReqFinish)}; 162 | if (value.GuestResFinish !== undefined) result.message = {field: "GuestResFinish", value: decodeJson_9(value.GuestResFinish)}; 163 | return result; 164 | } 165 | 166 | export function encodeBinary(value: $.pbkit.wrp.WrpMessage): Uint8Array { 167 | const result: WireMessage = []; 168 | switch (value.message?.field) { 169 | case "HostInitialize": { 170 | const tsValue = value.message.value; 171 | result.push( 172 | [1, { type: WireType.LengthDelimited as const, value: encodeBinary_1(tsValue) }], 173 | ); 174 | break; 175 | } 176 | case "HostError": { 177 | const tsValue = value.message.value; 178 | result.push( 179 | [2, { type: WireType.LengthDelimited as const, value: encodeBinary_2(tsValue) }], 180 | ); 181 | break; 182 | } 183 | case "HostResStart": { 184 | const tsValue = value.message.value; 185 | result.push( 186 | [3, { type: WireType.LengthDelimited as const, value: encodeBinary_3(tsValue) }], 187 | ); 188 | break; 189 | } 190 | case "HostResPayload": { 191 | const tsValue = value.message.value; 192 | result.push( 193 | [4, { type: WireType.LengthDelimited as const, value: encodeBinary_4(tsValue) }], 194 | ); 195 | break; 196 | } 197 | case "HostResFinish": { 198 | const tsValue = value.message.value; 199 | result.push( 200 | [5, { type: WireType.LengthDelimited as const, value: encodeBinary_5(tsValue) }], 201 | ); 202 | break; 203 | } 204 | case "GuestReqStart": { 205 | const tsValue = value.message.value; 206 | result.push( 207 | [6, { type: WireType.LengthDelimited as const, value: encodeBinary_6(tsValue) }], 208 | ); 209 | break; 210 | } 211 | case "GuestReqPayload": { 212 | const tsValue = value.message.value; 213 | result.push( 214 | [7, { type: WireType.LengthDelimited as const, value: encodeBinary_7(tsValue) }], 215 | ); 216 | break; 217 | } 218 | case "GuestReqFinish": { 219 | const tsValue = value.message.value; 220 | result.push( 221 | [8, { type: WireType.LengthDelimited as const, value: encodeBinary_8(tsValue) }], 222 | ); 223 | break; 224 | } 225 | case "GuestResFinish": { 226 | const tsValue = value.message.value; 227 | result.push( 228 | [9, { type: WireType.LengthDelimited as const, value: encodeBinary_9(tsValue) }], 229 | ); 230 | break; 231 | } 232 | } 233 | return serialize(result); 234 | } 235 | 236 | const fieldNames: Map = new Map([ 237 | [1, "HostInitialize"], 238 | [2, "HostError"], 239 | [3, "HostResStart"], 240 | [4, "HostResPayload"], 241 | [5, "HostResFinish"], 242 | [6, "GuestReqStart"], 243 | [7, "GuestReqPayload"], 244 | [8, "GuestReqFinish"], 245 | [9, "GuestResFinish"], 246 | ]); 247 | const oneofFieldNumbersMap: { [oneof: string]: Set } = { 248 | message: new Set([1, 2, 3, 4, 5, 6, 7, 8, 9]), 249 | }; 250 | const oneofFieldNamesMap = { 251 | message: new Map([ 252 | [1, "HostInitialize" as const], 253 | [2, "HostError" as const], 254 | [3, "HostResStart" as const], 255 | [4, "HostResPayload" as const], 256 | [5, "HostResFinish" as const], 257 | [6, "GuestReqStart" as const], 258 | [7, "GuestReqPayload" as const], 259 | [8, "GuestReqFinish" as const], 260 | [9, "GuestResFinish" as const], 261 | ]), 262 | }; 263 | export function decodeBinary(binary: Uint8Array): $.pbkit.wrp.WrpMessage { 264 | const result = getDefaultValue(); 265 | const wireMessage = deserialize(binary); 266 | const wireFields = new Map(wireMessage); 267 | const wireFieldNumbers = Array.from(wireFields.keys()).reverse(); 268 | oneof: { 269 | const oneofFieldNumbers = oneofFieldNumbersMap.message; 270 | const oneofFieldNames = oneofFieldNamesMap.message; 271 | const fieldNumber = wireFieldNumbers.find(v => oneofFieldNumbers.has(v)); 272 | if (fieldNumber == null) break oneof; 273 | const wireValue = wireFields.get(fieldNumber); 274 | const wireValueToTsValueMap = { 275 | [1](wireValue: Field) { return wireValue.type === WireType.LengthDelimited ? decodeBinary_1(wireValue.value) : undefined; }, 276 | [2](wireValue: Field) { return wireValue.type === WireType.LengthDelimited ? decodeBinary_2(wireValue.value) : undefined; }, 277 | [3](wireValue: Field) { return wireValue.type === WireType.LengthDelimited ? decodeBinary_3(wireValue.value) : undefined; }, 278 | [4](wireValue: Field) { return wireValue.type === WireType.LengthDelimited ? decodeBinary_4(wireValue.value) : undefined; }, 279 | [5](wireValue: Field) { return wireValue.type === WireType.LengthDelimited ? decodeBinary_5(wireValue.value) : undefined; }, 280 | [6](wireValue: Field) { return wireValue.type === WireType.LengthDelimited ? decodeBinary_6(wireValue.value) : undefined; }, 281 | [7](wireValue: Field) { return wireValue.type === WireType.LengthDelimited ? decodeBinary_7(wireValue.value) : undefined; }, 282 | [8](wireValue: Field) { return wireValue.type === WireType.LengthDelimited ? decodeBinary_8(wireValue.value) : undefined; }, 283 | [9](wireValue: Field) { return wireValue.type === WireType.LengthDelimited ? decodeBinary_9(wireValue.value) : undefined; }, 284 | }; 285 | const value = (wireValueToTsValueMap[fieldNumber as keyof typeof wireValueToTsValueMap] as any)?.(wireValue!); 286 | if (value === undefined) break oneof; 287 | result.message = { field: oneofFieldNames.get(fieldNumber)!, value: value as any }; 288 | } 289 | return result; 290 | } 291 | -------------------------------------------------------------------------------- /src/generated/messages/pbkit/wrp/index.ts: -------------------------------------------------------------------------------- 1 | export type { Type as WrpMessage } from "./WrpMessage.ts"; 2 | export type { Type as WrpHostMessage_Initialize } from "./WrpHostMessage_Initialize.ts"; 3 | export type { Type as WrpHostMessage_Error } from "./WrpHostMessage_Error.ts"; 4 | export type { Type as WrpHostMessage_ResStart } from "./WrpHostMessage_ResStart.ts"; 5 | export type { Type as WrpHostMessage_ResPayload } from "./WrpHostMessage_ResPayload.ts"; 6 | export type { Type as WrpHostMessage_ResFinish } from "./WrpHostMessage_ResFinish.ts"; 7 | export type { Type as WrpGuestMessage_ReqStart } from "./WrpGuestMessage_ReqStart.ts"; 8 | export type { Type as WrpGuestMessage_ReqPayload } from "./WrpGuestMessage_ReqPayload.ts"; 9 | export type { Type as WrpGuestMessage_ReqFinish } from "./WrpGuestMessage_ReqFinish.ts"; 10 | export type { Type as WrpGuestMessage_ResFinish } from "./WrpGuestMessage_ResFinish.ts"; 11 | -------------------------------------------------------------------------------- /src/glue/android.ts: -------------------------------------------------------------------------------- 1 | import { Socket } from "../socket.ts"; 2 | import { checkAndRetryUntilSuccess, u8s2str } from "./misc/util.ts"; 3 | import { getGlue } from "./index.ts"; 4 | 5 | // https://developer.android.com/reference/android/webkit/WebView#addJavascriptInterface(java.lang.Object,%20java.lang.String) 6 | // https://stackoverflow.com/a/45506857 7 | 8 | const key = ""; 9 | 10 | export async function createAndroidSocket(): Promise { 11 | const androidGlue = await getAndroidGlue(); 12 | return { 13 | read: getGlue().read, 14 | async write(data) { 15 | androidGlue.recv(u8s2str(data)); 16 | return data.byteLength; 17 | }, 18 | }; 19 | } 20 | 21 | interface AndroidGlue { 22 | recv(data: string): Promise; 23 | } 24 | async function getAndroidGlue(): Promise { 25 | return await checkAndRetryUntilSuccess( 26 | () => (globalThis as any)[key] || undefined, 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /src/glue/child-window.ts: -------------------------------------------------------------------------------- 1 | import { defer } from "https://deno.land/x/pbkit@v0.0.45/core/runtime/async/observer.ts"; 2 | import { Closer, Socket } from "../socket.ts"; 3 | import { createGlue } from "./index.ts"; 4 | import { 5 | isGlueEvent, 6 | isGlueHandshakeEvent, 7 | postGlueHandshakeMessage, 8 | postGlueMessage, 9 | } from "./misc/web.ts"; 10 | 11 | export interface CreateChildWindowSocketConfig { 12 | child: Window; 13 | childWindowOrigin: string; 14 | onClosed?: () => void; 15 | } 16 | export async function createChildWindowSocket( 17 | config: CreateChildWindowSocketConfig, 18 | ): Promise { 19 | const { child, childWindowOrigin, onClosed } = config; 20 | await handshake(child, childWindowOrigin); 21 | globalThis.addEventListener("message", messageHandler); 22 | const healthcheckId = setInterval(healthcheck, 100); 23 | const glue = createGlue(); 24 | return { 25 | read: glue.read, 26 | async write(data) { 27 | const length = data.byteLength; 28 | const success = postGlueMessage({ 29 | target: child, 30 | targetOrigin: childWindowOrigin, 31 | payload: data, 32 | }); 33 | if (!success) close(); 34 | return success ? length : 0; 35 | }, 36 | close, 37 | }; 38 | function close() { 39 | clearInterval(healthcheckId); 40 | globalThis.removeEventListener("message", messageHandler); 41 | glue.close(); 42 | onClosed?.(); 43 | } 44 | function healthcheck() { 45 | if (child.closed) close(); 46 | } 47 | function messageHandler(e: MessageEvent) { 48 | if (e.source !== child) return; 49 | if (!isGlueEvent(e)) return; 50 | const [, data] = e.data; 51 | glue.recv(data); 52 | } 53 | } 54 | 55 | // wait syn -> send syn-ack -> wait ack 56 | async function handshake(child: Window, childWindowOrigin: string) { 57 | let synAcked = false; 58 | const wait = defer(); 59 | const healthcheckId = setInterval(healthcheck, 100); 60 | const timeoutId = setTimeout(() => { 61 | abort(new Error("Handshake timeout.")); 62 | }, 10000); 63 | globalThis.addEventListener("message", handshakeHandler); 64 | function handshakeHandler(e: MessageEvent) { 65 | if (e.source !== child) return; 66 | if (!isGlueHandshakeEvent(e)) return; 67 | const [, data] = e.data; 68 | if (data === "syn") { 69 | synAck(); 70 | } else if (data === "ack") { 71 | if (synAcked) finish(); 72 | else abort(new Error("Invalid ack.")); 73 | } 74 | } 75 | return await wait; 76 | function healthcheck() { 77 | if (child.closed) abort(new Error("Child window is closed.")); 78 | } 79 | function cleanup() { 80 | clearInterval(healthcheckId); 81 | clearTimeout(timeoutId); 82 | globalThis.removeEventListener("message", handshakeHandler); 83 | } 84 | function abort(reason: Error) { 85 | cleanup(); 86 | wait.reject(reason); 87 | } 88 | function finish() { 89 | cleanup(); 90 | wait.resolve(); 91 | } 92 | function synAck() { 93 | synAcked = true; 94 | const success = postGlueHandshakeMessage({ 95 | target: child, 96 | targetOrigin: childWindowOrigin, 97 | payload: "syn-ack", 98 | }); 99 | if (!success) abort(new Error("Failed to send syn-ack.")); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/glue/iframe.ts: -------------------------------------------------------------------------------- 1 | import { Closer, Socket } from "../socket.ts"; 2 | import { checkAndRetryUntilSuccess } from "./misc/util.ts"; 3 | import { createChildWindowSocket } from "./child-window.ts"; 4 | 5 | export interface CreateIframeSocketConfig { 6 | iframeElement: HTMLIFrameElement; 7 | iframeOrigin: string; 8 | onClosed?: () => void; 9 | } 10 | export async function createIframeSocket( 11 | config: CreateIframeSocketConfig, 12 | ): Promise { 13 | const { iframeElement, iframeOrigin, onClosed } = config; 14 | const iframeWindow = await checkAndRetryUntilSuccess( 15 | () => iframeElement.contentWindow || undefined, 16 | ); 17 | const childWindowSocket = await createChildWindowSocket({ 18 | child: iframeWindow, 19 | childWindowOrigin: iframeOrigin, 20 | onClosed, 21 | }); 22 | onceIframeReloaded(iframeElement, childWindowSocket.close); 23 | return childWindowSocket; 24 | } 25 | 26 | function onceIframeReloaded(iframeElement: HTMLIFrameElement, fn: () => void) { 27 | iframeElement.addEventListener("load", loadHandler); 28 | function loadHandler() { 29 | iframeElement.removeEventListener("load", loadHandler); 30 | fn(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/glue/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | defer, 3 | Deferred, 4 | } from "https://deno.land/x/pbkit@v0.0.45/core/runtime/async/observer.ts"; 5 | import { Closer, Reader } from "../socket.ts"; 6 | import { chain } from "../misc.ts"; 7 | import { str2u8s } from "./misc/util.ts"; 8 | 9 | export interface Glue extends Closer, Reader { 10 | recv(data: Uint8Array | string): void; 11 | } 12 | 13 | const key = ""; 14 | 15 | export function getGlue(): Glue { 16 | const global = globalThis as any; 17 | if (global[key]) return global[key]; 18 | return global[key] = createGlue(); 19 | } 20 | 21 | export function createGlue(): Glue { 22 | const queue: Uint8Array[] = []; 23 | let closed = false; 24 | let wait: Deferred | undefined; 25 | return { 26 | close() { 27 | closed = true; 28 | wait?.resolve(); 29 | }, 30 | recv: (data) => { 31 | if (closed) throw new Error("Glue has been closed."); 32 | queue.push(typeof data === "string" ? str2u8s(data) : data); 33 | wait?.resolve(); 34 | }, 35 | read: chain(async (data) => { 36 | if (queue.length < 1) { 37 | if (closed) return null; 38 | await (wait = defer()); 39 | wait = undefined; 40 | } 41 | const first = queue[0]; 42 | if (first.byteLength <= data.byteLength) { 43 | queue.shift(); 44 | data.set(first); 45 | return first.byteLength; 46 | } 47 | data.set(first.subarray(0, data.byteLength)); 48 | queue[0] = first.subarray(data.byteLength); 49 | return data.byteLength; 50 | }), 51 | }; 52 | } 53 | -------------------------------------------------------------------------------- /src/glue/ios.ts: -------------------------------------------------------------------------------- 1 | import { Socket } from "../socket.ts"; 2 | import { checkAndRetryUntilSuccess, u8s2str } from "./misc/util.ts"; 3 | import { getGlue } from "./index.ts"; 4 | 5 | // https://developer.apple.com/documentation/webkit/wkusercontentcontroller/1537172-add 6 | 7 | export async function createIosSocket(): Promise { 8 | const iosGlue = await getIosGlue(); 9 | return { 10 | read: getGlue().read, 11 | async write(data) { 12 | return iosGlue.postMessage(u8s2str(data)).then(() => data.byteLength); 13 | }, 14 | }; 15 | } 16 | 17 | interface IosGlue { 18 | postMessage(data: string): Promise; 19 | } 20 | async function getIosGlue(): Promise { 21 | return await checkAndRetryUntilSuccess( 22 | () => (globalThis as any).webkit?.messageHandlers?.glue || undefined, 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /src/glue/misc/util.ts: -------------------------------------------------------------------------------- 1 | export function u8s2str(u8s: Uint8Array): string { 2 | return u8s.reduce((acc, curr) => acc + String.fromCharCode(curr), ""); 3 | } 4 | 5 | export function str2u8s(str: string): Uint8Array { 6 | const u8s = new Uint8Array(str.length); 7 | for (let i = 0; i < str.length; ++i) u8s[i] = str.charCodeAt(i); 8 | return u8s; 9 | } 10 | 11 | export function checkAndRetryUntilSuccess( 12 | checkFn: () => T | undefined, 13 | retryFn?: () => void, 14 | interval: number = 100, 15 | limit: number = 10, 16 | ): Promise { 17 | return new Promise((resolve, reject) => { 18 | let count = 0; 19 | const intervalId = setInterval(() => { 20 | const result = checkFn(); 21 | if (result !== undefined) { 22 | clearInterval(intervalId); 23 | resolve(result); 24 | return; 25 | } 26 | if (count++ < limit) { 27 | retryFn?.(); 28 | } else { 29 | clearInterval(intervalId); 30 | reject(new Error("Exhausted all retries")); 31 | } 32 | }, interval); 33 | }); 34 | } 35 | -------------------------------------------------------------------------------- /src/glue/misc/web.ts: -------------------------------------------------------------------------------- 1 | export const eventKey = ""; 2 | export const handshakeEventKey = ""; 3 | 4 | export interface GlueEvent { 5 | data: [ 6 | glueKey: typeof eventKey, 7 | payload: Uint8Array, 8 | ]; 9 | source: typeof globalThis | Window; 10 | } 11 | export function isGlueEvent(event: any): event is GlueEvent { 12 | if (!Array.isArray(event.data)) return false; 13 | if (event.data.length < 2) return false; 14 | if (event.data[0] !== eventKey) return false; 15 | return true; 16 | } 17 | 18 | export interface GlueHandshakeEvent { 19 | data: [ 20 | glueKey: typeof handshakeEventKey, 21 | payload: "syn" | "syn-ack" | "ack", 22 | ]; 23 | source: typeof globalThis | Window; 24 | } 25 | export function isGlueHandshakeEvent(event: any): event is GlueHandshakeEvent { 26 | if (!Array.isArray(event.data)) return false; 27 | if (event.data.length < 2) return false; 28 | if (event.data[0] !== handshakeEventKey) return false; 29 | return true; 30 | } 31 | 32 | export interface PostGlueHandshakeMessageConfig { 33 | target: Window; 34 | targetOrigin: string; 35 | payload: GlueHandshakeEvent["data"]["1"]; 36 | } 37 | export function postGlueHandshakeMessage( 38 | config: PostGlueHandshakeMessageConfig, 39 | ): boolean { 40 | const { target, targetOrigin, payload } = config; 41 | if (target.closed) return false; 42 | target.postMessage([handshakeEventKey, payload], targetOrigin); 43 | return true; 44 | } 45 | 46 | export interface PostGlueMessageConfig { 47 | target: Window; 48 | targetOrigin: string; 49 | payload: Uint8Array; 50 | } 51 | export function postGlueMessage(config: PostGlueMessageConfig): boolean { 52 | const { target, targetOrigin, payload } = config; 53 | if (target.closed) return false; 54 | target.postMessage([eventKey, payload], targetOrigin, [payload.buffer]); 55 | return true; 56 | } 57 | -------------------------------------------------------------------------------- /src/glue/parent-window.ts: -------------------------------------------------------------------------------- 1 | import { defer } from "https://deno.land/x/pbkit@v0.0.45/core/runtime/async/observer.ts"; 2 | import { Closer, Socket } from "../socket.ts"; 3 | import { getGlue } from "./index.ts"; 4 | import { 5 | isGlueEvent, 6 | isGlueHandshakeEvent, 7 | postGlueHandshakeMessage, 8 | postGlueMessage, 9 | } from "./misc/web.ts"; 10 | 11 | // https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API 12 | // https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage 13 | 14 | export interface CreateParentWindowSocketConfig { 15 | parent?: Window | null; 16 | parentWindowOrigin: string; 17 | onClosed?: () => void; 18 | } 19 | export async function createParentWindowSocket( 20 | config: CreateParentWindowSocketConfig, 21 | ): Promise { 22 | const { parent = globalThis.parent, parentWindowOrigin, onClosed } = config; 23 | if (!parent?.postMessage) throw new Error("There is no parent window."); 24 | if (parent === globalThis.self) throw new Error("Invalid parent window."); 25 | const wait = defer(); 26 | let acked = false; 27 | globalThis.addEventListener("message", messageHandler); 28 | globalThis.addEventListener("message", handshakeHandler); 29 | const glue = getGlue(); 30 | const connId = setInterval(syn, 100); 31 | syn(); 32 | await wait; 33 | return { 34 | read: glue.read, 35 | async write(data) { 36 | const length = data.byteLength; 37 | const success = postGlueMessage({ 38 | target: parent, 39 | targetOrigin: parentWindowOrigin, 40 | payload: data, 41 | }); 42 | if (!success) close(); 43 | return success ? length : 0; 44 | }, 45 | close, 46 | }; 47 | function close() { 48 | clearInterval(connId); 49 | globalThis.removeEventListener("message", messageHandler); 50 | globalThis.removeEventListener("message", handshakeHandler); 51 | glue.close(); 52 | onClosed?.(); 53 | wait.reject(new Error("Connection closed.")); 54 | } 55 | function syn() { 56 | const success = postGlueHandshakeMessage({ 57 | target: parent!, 58 | targetOrigin: parentWindowOrigin, 59 | payload: "syn", 60 | }); 61 | if (!success) close(); 62 | } 63 | function ack() { 64 | acked = true; 65 | const success = postGlueHandshakeMessage({ 66 | target: parent!, 67 | targetOrigin: parentWindowOrigin, 68 | payload: "ack", 69 | }); 70 | if (success) wait.resolve(); 71 | else close(); 72 | } 73 | function messageHandler(e: MessageEvent) { 74 | if (e.source !== parent) return; 75 | if (!isGlueEvent(e)) return; 76 | const [, data] = e.data; 77 | glue.recv(data); 78 | } 79 | function handshakeHandler(event: any) { 80 | if (event.source !== parent) return; 81 | if (!isGlueHandshakeEvent(event)) return; 82 | const [, payload] = event.data; 83 | if (payload === "syn-ack") { 84 | if (acked) close(); 85 | else ack(); 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/glue/parent.ts: -------------------------------------------------------------------------------- 1 | import { defer } from "https://deno.land/x/pbkit@v0.0.45/core/runtime/async/observer.ts"; 2 | import { Socket } from "../socket.ts"; 3 | import { createIosSocket } from "../glue/ios.ts"; 4 | import { createAndroidSocket } from "../glue/android.ts"; 5 | import { createParentWindowSocket } from "../glue/parent-window.ts"; 6 | 7 | export type UnsubscribeFn = () => void; 8 | export type SetSocketFn = (socket?: Socket, error?: Error) => void; 9 | export function subscribeParentSocket(set: SetSocketFn): UnsubscribeFn { 10 | let run = true; 11 | const unsubscribe = () => (run = false); 12 | const parent = globalThis.opener || globalThis.parent; 13 | if (parent && parent !== globalThis.self) { 14 | (async () => { 15 | while (run) { 16 | const closed = defer(); 17 | try { 18 | const socket = await createParentWindowSocket({ 19 | parent, 20 | parentWindowOrigin: "*", 21 | onClosed: () => closed.resolve(), 22 | }); 23 | run && set(socket, undefined); 24 | } catch (error) { 25 | run && set(undefined, error); 26 | } 27 | await closed; 28 | run && set(undefined, undefined); 29 | } 30 | })(); 31 | } else { 32 | getAndroidOrIosSocket().then( 33 | (socket) => run && set(socket), 34 | ).catch( 35 | (error) => run && set(undefined, error), 36 | ); 37 | } 38 | return unsubscribe; 39 | } 40 | 41 | async function getAndroidOrIosSocket(): Promise { 42 | try { 43 | return await Promise.any([createAndroidSocket(), createIosSocket()]); 44 | } catch {} 45 | return; 46 | } 47 | -------------------------------------------------------------------------------- /src/guest.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createEventBuffer, 3 | EventBuffer, 4 | } from "https://deno.land/x/pbkit@v0.0.45/core/runtime/async/event-buffer.ts"; 5 | import { 6 | defer, 7 | Deferred, 8 | } from "https://deno.land/x/pbkit@v0.0.45/core/runtime/async/observer.ts"; 9 | import type { WrpChannel } from "./channel.ts"; 10 | import { LazyMetadata, Metadata, resolveLazyMetadata } from "./metadata.ts"; 11 | 12 | export interface WrpGuest { 13 | readonly availableMethods: Set; 14 | request( 15 | methodName: string, 16 | req: AsyncGenerator, 17 | metadata?: LazyMetadata, 18 | ): { 19 | res: AsyncGenerator; 20 | header: Promise; 21 | trailer: Promise; 22 | }; 23 | } 24 | 25 | export class WrpError extends Error { 26 | constructor(public message: string) { 27 | super(message); 28 | } 29 | } 30 | 31 | export interface CreateWrpGuestConfig { 32 | channel: WrpChannel; 33 | onError?: (error: WrpError) => void; 34 | } 35 | export async function createWrpGuest( 36 | config: CreateWrpGuestConfig, 37 | ): Promise { 38 | const { channel } = config; 39 | let availableMethods: Set; 40 | const waitFirstHostInitialize = defer(); 41 | interface Request { 42 | eventBuffer: EventBuffer; 43 | header: Deferred; 44 | trailer: Deferred; 45 | } 46 | const requests: { [reqId: string]: Request } = {}; 47 | function finishRequest(reqId: string, trailerObject: Record) { 48 | const request = requests[reqId]; 49 | request.trailer.resolve(trailerObject); 50 | if (trailerObject["wrp-status"] === "ok") { 51 | request.eventBuffer.finish(); 52 | } else { 53 | const message = trailerObject["wrp-message"] || ""; 54 | request.eventBuffer.error(new WrpError(message)); 55 | } 56 | delete requests[reqId]; 57 | } 58 | let reqIdCounter = 0; 59 | (async () => { 60 | for await (const { message } of channel.listen()) { 61 | if (message == null) continue; 62 | switch (message.field) { 63 | default: 64 | continue; 65 | case "HostInitialize": { 66 | const { value } = message; 67 | availableMethods = new Set(value.availableMethods); 68 | waitFirstHostInitialize.resolve(); 69 | // handle host reconnection 70 | for (const reqId in requests) { 71 | finishRequest(reqId, { 72 | "wrp-status": "error", 73 | "wrp-message": "Host disconnected and reconnected.", 74 | }); 75 | } 76 | continue; 77 | } 78 | case "HostError": { 79 | config.onError?.(new WrpError(message.value.message)); 80 | continue; 81 | } 82 | case "HostResStart": { 83 | const { reqId, header } = message.value; 84 | requests[reqId]?.header.resolve(Object.fromEntries(header)); 85 | continue; 86 | } 87 | case "HostResPayload": { 88 | const { reqId, payload } = message.value; 89 | requests[reqId]?.eventBuffer.push(payload); 90 | continue; 91 | } 92 | case "HostResFinish": { 93 | const { reqId, trailer } = message.value; 94 | if (!(reqId in requests)) continue; 95 | finishRequest(reqId, Object.fromEntries(trailer)); 96 | continue; 97 | } 98 | } 99 | } 100 | })(); 101 | await waitFirstHostInitialize; 102 | return { 103 | get availableMethods() { 104 | return availableMethods; 105 | }, 106 | request(methodName, req, lazyMetadata) { 107 | const reqId = `${++reqIdCounter}`; 108 | const eventBuffer = createEventBuffer({ 109 | onDrainEnd() { 110 | channel.send({ 111 | message: { 112 | field: "GuestResFinish", 113 | value: { reqId }, 114 | }, 115 | }); 116 | }, 117 | }); 118 | const header = defer(); 119 | const trailer = defer(); 120 | const res = eventBuffer.drain(); 121 | requests[reqId] = { eventBuffer, header, trailer }; 122 | (async () => { 123 | try { 124 | const metadata = new Map(Object.entries( 125 | await resolveLazyMetadata(lazyMetadata), 126 | )); 127 | channel.send({ 128 | message: { 129 | field: "GuestReqStart", 130 | value: { reqId, methodName, metadata }, 131 | }, 132 | }); 133 | for await (const payload of req) { 134 | channel.send({ 135 | message: { 136 | field: "GuestReqPayload", 137 | value: { reqId, payload }, 138 | }, 139 | }); 140 | } 141 | } finally { 142 | channel.send({ 143 | message: { 144 | field: "GuestReqFinish", 145 | value: { reqId }, 146 | }, 147 | }); 148 | } 149 | })(); 150 | return { res, header, trailer }; 151 | }, 152 | }; 153 | } 154 | -------------------------------------------------------------------------------- /src/host.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createEventBuffer, 3 | EventBuffer, 4 | } from "https://deno.land/x/pbkit@v0.0.45/core/runtime/async/event-buffer.ts"; 5 | import { 6 | createEventEmitter, 7 | EventEmitter, 8 | } from "https://deno.land/x/pbkit@v0.0.45/core/runtime/async/event-emitter.ts"; 9 | import type { WrpChannel } from "./channel.ts"; 10 | import type { Metadata } from "./metadata.ts"; 11 | 12 | export interface WrpHost { 13 | listen(): AsyncGenerator; 14 | } 15 | export interface WrpRequest extends EventEmitter { 16 | methodName: string; 17 | metadata: Metadata; 18 | req: AsyncGenerator; 19 | sendHeader(value: Metadata): void; 20 | sendPayload(value: Uint8Array): void; 21 | sendTrailer(value: Metadata): void; 22 | } 23 | interface WrpRequestEvents { 24 | "cancel-response": void; 25 | "close": void; 26 | } 27 | 28 | interface RequestHandlingState { 29 | reqId: string; 30 | request: WrpRequest; 31 | eventBuffer: EventBuffer; 32 | reqFinished: boolean; 33 | resFinished: boolean; 34 | } 35 | interface RequestHandlingStateTable { 36 | [reqId: string]: RequestHandlingState; 37 | } 38 | 39 | export interface CreateWrpHostConfig { 40 | channel: WrpChannel; 41 | availableMethods: Set; 42 | } 43 | export async function createWrpHost( 44 | config: CreateWrpHostConfig, 45 | ): Promise { 46 | const { channel, availableMethods } = config; 47 | const states: RequestHandlingStateTable = {}; 48 | function tryForgetState(state: RequestHandlingState): void { 49 | if (!state.reqFinished) return; 50 | if (!state.resFinished) return; 51 | delete states[state.reqId]; 52 | } 53 | await channel.send({ 54 | message: { 55 | field: "HostInitialize", 56 | value: { availableMethods: Array.from(availableMethods) }, 57 | }, 58 | }); 59 | return { 60 | async *listen() { 61 | try { 62 | for await (const { message } of channel.listen()) { 63 | if (message == null) { 64 | channel.send({ 65 | message: { 66 | field: "HostError", 67 | value: { message: "Received null message" }, 68 | }, 69 | }); 70 | continue; 71 | } 72 | switch (message.field) { 73 | case "GuestReqStart": { 74 | const { reqId, methodName, metadata } = message.value; 75 | const eventBuffer = createEventBuffer({ 76 | onDrainEnd() { 77 | state.reqFinished = true; 78 | tryForgetState(state); 79 | }, 80 | }); 81 | const request = createWrpRequest({ 82 | methodName, 83 | channel, 84 | reqId, 85 | req: eventBuffer.drain(), 86 | metadata: Object.fromEntries(metadata), 87 | }); 88 | const state: RequestHandlingState = states[reqId] = { 89 | reqId, 90 | request, 91 | eventBuffer, 92 | reqFinished: false, 93 | resFinished: false, 94 | }; 95 | yield request; 96 | continue; 97 | } 98 | case "GuestReqPayload": { 99 | const { reqId, payload } = message.value; 100 | processMessage(channel, states, reqId, (state) => { 101 | state.eventBuffer.push(payload); 102 | }); 103 | continue; 104 | } 105 | case "GuestReqFinish": { 106 | const { reqId } = message.value; 107 | processMessage(channel, states, reqId, (state) => { 108 | state.eventBuffer.finish(); 109 | state.reqFinished = true; 110 | tryForgetState(state); 111 | }); 112 | continue; 113 | } 114 | case "GuestResFinish": { 115 | const { reqId } = message.value; 116 | processMessage(channel, states, reqId, (state) => { 117 | state.request.emit("cancel-response", undefined); 118 | state.resFinished = true; 119 | tryForgetState(state); 120 | }); 121 | continue; 122 | } 123 | } 124 | } 125 | } finally { 126 | for (const reqId in states) { 127 | states[reqId].request.emit("close", undefined); 128 | delete states[reqId]; 129 | } 130 | } 131 | }, 132 | }; 133 | } 134 | 135 | function processMessage( 136 | channel: WrpChannel, 137 | states: RequestHandlingStateTable, 138 | reqId: string, 139 | fn: (request: RequestHandlingState) => void, 140 | ): void { 141 | if (reqId in states) { 142 | fn(states[reqId]); 143 | } else { 144 | channel.send({ 145 | message: { 146 | field: "HostError", 147 | value: { 148 | message: 149 | `Received unexpected request finish for { reqId: "${reqId}" }`, 150 | }, 151 | }, 152 | }); 153 | } 154 | } 155 | 156 | interface CreateWrpRequestConfig { 157 | methodName: string; 158 | channel: WrpChannel; 159 | reqId: string; 160 | req: AsyncGenerator; 161 | metadata: Metadata; 162 | } 163 | function createWrpRequest(config: CreateWrpRequestConfig): WrpRequest { 164 | const { methodName, channel, reqId, req, metadata } = config; 165 | return { 166 | ...createEventEmitter(), 167 | methodName, 168 | metadata, 169 | req, 170 | sendHeader(header) { 171 | channel.send({ 172 | message: { 173 | field: "HostResStart", 174 | value: { reqId, header: new Map(Object.entries(header)) }, 175 | }, 176 | }); 177 | }, 178 | sendPayload(payload) { 179 | channel.send({ 180 | message: { 181 | field: "HostResPayload", 182 | value: { reqId, payload }, 183 | }, 184 | }); 185 | }, 186 | sendTrailer(trailer) { 187 | trailer["wrp-status"] ||= "ok"; 188 | trailer["wrp-message"] ||= ""; 189 | channel.send({ 190 | message: { 191 | field: "HostResFinish", 192 | value: { reqId, trailer: new Map(Object.entries(trailer)) }, 193 | }, 194 | }); 195 | }, 196 | }; 197 | } 198 | -------------------------------------------------------------------------------- /src/jotai/iframe.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import { atom, PrimitiveAtom, useSetAtom } from "jotai"; 3 | import { 4 | createWrpAtomSetFromSourceChannelAtom, 5 | PrimitiveSocketAtom, 6 | WrpAtomSet, 7 | } from "./index.ts"; 8 | import { Socket } from "../socket.ts"; 9 | import { 10 | createWrpChannel as createWrpChannelFn, 11 | WrpChannel, 12 | } from "../channel.ts"; 13 | import { 14 | default as useWrpIframeSocket, 15 | UseWrpIframeSocketResult, 16 | } from "../react/useWrpIframeSocket.ts"; 17 | 18 | export function useIframeWrpSocketAtomUpdateEffect( 19 | socketAtom: PrimitiveSocketAtom, 20 | ): UseWrpIframeSocketResult { 21 | const setSocket = useSetAtom(socketAtom); 22 | const useWrpIframeSocketResult = useWrpIframeSocket(); 23 | const { socket } = useWrpIframeSocketResult; 24 | useEffect(() => { 25 | if (!socket) return; 26 | setSocket(socket); 27 | }, [socket]); 28 | return useWrpIframeSocketResult; 29 | } 30 | 31 | export function useIframeWrpAtomSetUpdateEffect( 32 | primitiveWrpAtomSetAtom: PrimitiveAtom, 33 | createWrpChannel: (socket: Socket) => WrpChannel = createWrpChannelFn, 34 | ): UseWrpIframeSocketResult { 35 | const setIframeAtomSet = useSetAtom(primitiveWrpAtomSetAtom); 36 | const useWrpIframeSocketResult = useWrpIframeSocket(); 37 | const { socket } = useWrpIframeSocketResult; 38 | useEffect(() => { 39 | if (!socket) return; 40 | const channelAtom = atom(createWrpChannel(socket)); 41 | setIframeAtomSet(createWrpAtomSetFromSourceChannelAtom(channelAtom)); 42 | }, [socket]); 43 | return useWrpIframeSocketResult; 44 | } 45 | -------------------------------------------------------------------------------- /src/jotai/index.ts: -------------------------------------------------------------------------------- 1 | import { Atom, atom, PrimitiveAtom } from "jotai"; 2 | import { selectAtom } from "jotai/utils"; 3 | import type { RpcClientImpl } from "https://deno.land/x/pbkit@v0.0.45/core/runtime/rpc.ts"; 4 | import { Socket } from "../socket.ts"; 5 | import { createWrpChannel, WrpChannel } from "../channel.ts"; 6 | import { createWrpClientImpl } from "../rpc/client.ts"; 7 | import { WrpGuest } from "../guest.ts"; 8 | import tee from "../tee.ts"; 9 | 10 | export type SocketAtom = PrimitiveSocketAtom | AsyncSocketAtom; 11 | export type ChannelAtom = Atom; 12 | export type GuestAtom = Atom | undefined>; 13 | export type ClientImplAtom = Atom; 14 | 15 | export type PrimitiveSocketAtom = PrimitiveAtom; 16 | export type AsyncSocketAtom = Atom>; 17 | 18 | export interface WrpAtomSet { 19 | channelAtom: ChannelAtom; 20 | guestAtom: GuestAtom; 21 | clientImplAtom: ClientImplAtom; 22 | } 23 | export function createWrpAtomSet(socketAtom: SocketAtom): WrpAtomSet { 24 | const channelAtom: ChannelAtom = atom((get) => { 25 | const socket = get(socketAtom); 26 | if (!socket) return; 27 | return createWrpChannel(socket); 28 | }); 29 | return createWrpAtomSetFromSourceChannelAtom(channelAtom); 30 | } 31 | 32 | export function createWrpAtomSetFromSourceChannelAtom( 33 | sourceChannelAtom: ChannelAtom, 34 | ): WrpAtomSet { 35 | interface ChannelAndGuest { 36 | channel: WrpChannel; 37 | guest: Promise; 38 | } 39 | const channelAndGuestAtom = atom>( 40 | async (get) => { 41 | const sourceChannel = get(sourceChannelAtom); 42 | if (!sourceChannel) return; 43 | return tee(sourceChannel); 44 | }, 45 | ); 46 | const channelAtom = selectAtom( 47 | channelAndGuestAtom, 48 | (cag) => cag?.channel, 49 | ); 50 | const guestAtom = selectAtom( 51 | channelAndGuestAtom, 52 | (cag) => cag?.guest, 53 | ); 54 | const clientImplAtom = atom((get) => { 55 | const guest = get(guestAtom); 56 | if (!guest) return; 57 | return createWrpClientImpl({ guest: guest }); 58 | }); 59 | return { channelAtom, guestAtom, clientImplAtom }; 60 | } 61 | -------------------------------------------------------------------------------- /src/jotai/parent.ts: -------------------------------------------------------------------------------- 1 | import { atom, useAtomValue, useSetAtom } from "jotai"; 2 | import type { RpcClientImpl } from "https://deno.land/x/pbkit@v0.0.45/core/runtime/rpc.ts"; 3 | import { Socket } from "../socket.ts"; 4 | import { WrpChannel } from "../channel.ts"; 5 | import { WrpGuest } from "../guest.ts"; 6 | import useOnceEffect from "../react/useOnceEffect.ts"; 7 | import { subscribeParentSocket } from "../glue/parent.ts"; 8 | import { 9 | ChannelAtom, 10 | ClientImplAtom, 11 | createWrpAtomSet, 12 | GuestAtom, 13 | PrimitiveSocketAtom, 14 | WrpAtomSet, 15 | } from "./index.ts"; 16 | 17 | /** 18 | * Use it on root of your react application 19 | */ 20 | export function useInitParentSocketEffect() { 21 | const setSocket = useSetAtom(socketAtom); 22 | useOnceEffect(() => subscribeParentSocket(setSocket)); 23 | } 24 | 25 | export const socketAtom: PrimitiveSocketAtom = atom( 26 | undefined, 27 | ); 28 | 29 | const wrpAtomSet: WrpAtomSet = createWrpAtomSet(socketAtom); 30 | 31 | export const channelAtom: ChannelAtom = wrpAtomSet.channelAtom; 32 | export function useChannel(): WrpChannel | undefined { 33 | return useAtomValue(wrpAtomSet.channelAtom); 34 | } 35 | 36 | export const guestAtom: GuestAtom = wrpAtomSet.guestAtom; 37 | export function useGuest(): WrpGuest | undefined { 38 | return useAtomValue(wrpAtomSet.guestAtom); 39 | } 40 | 41 | export const clientImplAtom: ClientImplAtom = wrpAtomSet.clientImplAtom; 42 | export function useClientImpl(): RpcClientImpl | undefined { 43 | return useAtomValue(wrpAtomSet.clientImplAtom); 44 | } 45 | -------------------------------------------------------------------------------- /src/jotai/pwasa.ts: -------------------------------------------------------------------------------- 1 | import { atom, PrimitiveAtom, useAtomValue } from "jotai"; 2 | import type { RpcClientImpl } from "https://deno.land/x/pbkit@v0.0.45/core/runtime/rpc.ts"; 3 | import { WrpChannel } from "../channel.ts"; 4 | import { WrpGuest } from "../guest.ts"; 5 | import { WrpAtomSet } from "./index.ts"; 6 | 7 | export type PrimitiveWrpAtomSetAtom = PrimitiveAtom; 8 | export function createPrimitiveWrpAtomSetAtom(): PrimitiveWrpAtomSetAtom { 9 | return atom({ 10 | channelAtom: atom(undefined), 11 | guestAtom: atom(undefined), 12 | clientImplAtom: atom(undefined), 13 | }); 14 | } 15 | export function useChannel( 16 | primitiveWrpAtomSetAtom: PrimitiveAtom, 17 | ): WrpChannel | undefined { 18 | const wrpAtomSet = useAtomValue(primitiveWrpAtomSetAtom); 19 | const channel = useAtomValue(wrpAtomSet.channelAtom); 20 | return channel; 21 | } 22 | export function useGuest( 23 | primitiveWrpAtomSetAtom: PrimitiveAtom, 24 | ): WrpGuest | undefined { 25 | const wrpAtomSet = useAtomValue(primitiveWrpAtomSetAtom); 26 | const guest = useAtomValue(wrpAtomSet.guestAtom); 27 | return guest; 28 | } 29 | export function useClientImpl( 30 | primitiveWrpAtomSetAtom: PrimitiveAtom, 31 | ): RpcClientImpl | undefined { 32 | const wrpAtomSet = useAtomValue(primitiveWrpAtomSetAtom); 33 | const clientImpl = useAtomValue(wrpAtomSet.clientImplAtom); 34 | return clientImpl; 35 | } 36 | -------------------------------------------------------------------------------- /src/metadata.ts: -------------------------------------------------------------------------------- 1 | export type Metadata = Record; 2 | 3 | export interface LazyMetadata { 4 | [key: string]: 5 | | undefined 6 | | string 7 | | Promise 8 | | (() => string | undefined) 9 | | (() => Promise); 10 | } 11 | 12 | export async function resolveLazyMetadata( 13 | lazyMetadata?: LazyMetadata, 14 | ): Promise { 15 | if (!lazyMetadata) return {}; 16 | return Object.fromEntries( 17 | await Promise.all( 18 | dropNullishEntries(Object.entries(lazyMetadata)).map( 19 | async (entry) => { 20 | const [key, value] = entry; 21 | if (value instanceof Promise) [key, await value] as typeof entry; 22 | if (typeof value !== "function") return entry; 23 | else return [key, await value()] as typeof entry; 24 | }, 25 | ), 26 | ).then(dropNullishEntries), 27 | ); 28 | function dropNullishEntries(entries: [string, any][]) { 29 | return entries.filter((entry) => entry[1] != null); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/misc.ts: -------------------------------------------------------------------------------- 1 | export function chain Promise>(fn: T): T { 2 | let prev: Promise | undefined; 3 | return function _fn(...args) { 4 | const curr = (prev || Promise.resolve()) 5 | .then(() => fn(...args)) 6 | .then((result) => { 7 | if (prev === curr) prev = undefined; 8 | return result; 9 | }); 10 | return (prev = curr); 11 | } as T; 12 | } 13 | -------------------------------------------------------------------------------- /src/react/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./useWrpClientImpl.ts"; 2 | export { default as useWrpClientImpl } from "./useWrpClientImpl.ts"; 3 | export * from "./useWrpIframeSocket.ts"; 4 | export { default as useWrpIframeSocket } from "./useWrpIframeSocket.ts"; 5 | export * from "./useWrpParentSocket.ts"; 6 | export { default as useWrpParentSocket } from "./useWrpParentSocket.ts"; 7 | export * as server from "./server.ts"; 8 | -------------------------------------------------------------------------------- /src/react/server.ts: -------------------------------------------------------------------------------- 1 | import { 2 | MethodDescriptor, 3 | MethodImplHandlerReq, 4 | } from "https://deno.land/x/pbkit@v0.0.45/core/runtime/rpc.ts"; 5 | import { first } from "https://deno.land/x/pbkit@v0.0.45/core/runtime/async/async-generator.ts"; 6 | import { createEventBuffer } from "https://deno.land/x/pbkit@v0.0.45/core/runtime/async/event-buffer.ts"; 7 | import { 8 | createEventEmitter, 9 | EventEmitter, 10 | } from "https://deno.land/x/pbkit@v0.0.45/core/runtime/async/event-emitter.ts"; 11 | import { useEffect, useRef } from "react"; 12 | import { WrpChannel } from "../channel.ts"; 13 | import { createWrpHost, WrpRequest } from "../host.ts"; 14 | import { createWrpServer, createWrpServerImplBuilder } from "../rpc/server.ts"; 15 | 16 | export function useWrpServer>( 17 | channel: WrpChannel | undefined, 18 | state: TState, 19 | ...rpcs: MethodImpl>[] 20 | ) { 21 | const ref = useRef | undefined>(undefined); 22 | useEffect(() => { 23 | if (!channel) return; 24 | if (!ref.current) ref.current = createRef(); 25 | const getState = () => ref.current?.state!; 26 | const changes = ref.current.changes; 27 | (async () => { 28 | const host = await createWrpHost({ 29 | channel, 30 | availableMethods: new Set( 31 | rpcs.map(({ methodDescriptor: { service, methodName } }) => 32 | `${service.serviceName}/${methodName}` 33 | ), 34 | ), 35 | }); 36 | const builder = createWrpServerImplBuilder(); 37 | for (const { methodDescriptor, handler } of rpcs) { 38 | builder.register( 39 | methodDescriptor, 40 | async (req, res) => { 41 | const { requestStream, responseStream } = methodDescriptor; 42 | const { teardown, callTeardown } = getTeardown(req); 43 | const stateChanges = new Proxy({}, { 44 | get(_, key) { 45 | const eb = createEventBuffer(); 46 | teardown(changes.on(key as keyof TState, eb.push)); 47 | teardown(eb.finish); 48 | return eb.drain(); 49 | }, 50 | }) as StateChanges; 51 | try { 52 | res.header({}); 53 | const _req = requestStream ? req.messages : first(req.messages); 54 | const _res = handler({ 55 | req: _req, 56 | getState, 57 | stateChanges, 58 | teardown, 59 | }); 60 | if (responseStream) { 61 | for await (const value of _res as AsyncGenerator) { 62 | res.send(value); 63 | } 64 | } else { 65 | res.send(await _res); 66 | } 67 | res.end({}); 68 | } catch (err) { 69 | res.end({ 70 | "wrp-status": "error", 71 | "wrp-message": err?.message ?? "", 72 | }); 73 | } finally { 74 | callTeardown(); 75 | } 76 | }, 77 | ); 78 | } 79 | builder.finish(); 80 | const methods = builder.drain(); 81 | const server = await createWrpServer({ host, methods }); 82 | server.listen(); 83 | })(); 84 | }, [channel]); 85 | useEffect(() => { 86 | if (!ref.current) ref.current = createRef(); 87 | const prev = { ...ref.current.state }; 88 | ref.current.state = state; 89 | for (const key of new Set([...Object.keys(prev), ...Object.keys(state)])) { 90 | if (prev[key] !== state[key]) { 91 | ref.current.changes.emit(key, state[key]); 92 | } 93 | } 94 | }, [state]); 95 | } 96 | 97 | interface Ref { 98 | state: TState; 99 | changes: EventEmitter; 100 | } 101 | function createRef(): Ref { 102 | return { 103 | state: undefined as unknown as TState, 104 | changes: createEventEmitter(), 105 | }; 106 | } 107 | 108 | export function rpc< 109 | TState extends Record, 110 | TMethodDescriptor extends MethodDescriptor, 111 | >( 112 | methodDescriptor: TMethodDescriptor, 113 | handler: RpcHandler< 114 | TState, 115 | TMethodDescriptor["requestStream"], 116 | TMethodDescriptor["responseStream"], 117 | Parameters[0], 118 | Parameters[0] 119 | >, 120 | ): MethodImpl { 121 | return { methodDescriptor, handler }; 122 | } 123 | 124 | export type GetStateFn> = () => TState; 125 | 126 | export type StateChanges> = { 127 | [key in keyof TState]: AsyncGenerator; 128 | }; 129 | 130 | export type TeardownFn = (handler: () => void) => void; 131 | 132 | interface GetTeardownResult { 133 | teardown: TeardownFn; 134 | callTeardown: () => void; 135 | } 136 | function getTeardown( 137 | req: MethodImplHandlerReq, 138 | ): GetTeardownResult { 139 | const handlers: (() => void)[] = []; 140 | const teardown: TeardownFn = (handler) => handlers.push(handler); 141 | const o1 = req.metadata?.on("cancel-response", callTeardown); 142 | const o2 = req.metadata?.on("close", callTeardown); 143 | const offAll = () => (o1?.(), o2?.()); 144 | function callTeardown() { 145 | offAll(); 146 | for (const handler of handlers) handler(); 147 | handlers.length = 0; 148 | } 149 | return { teardown, callTeardown }; 150 | } 151 | 152 | export interface MethodImpl< 153 | TState, 154 | TMethodDescriptor extends MethodDescriptor, 155 | > { 156 | methodDescriptor: TMethodDescriptor; 157 | handler: RpcHandler< 158 | TState, 159 | TMethodDescriptor["requestStream"], 160 | TMethodDescriptor["responseStream"], 161 | Parameters[0], 162 | Parameters[0] 163 | >; 164 | } 165 | 166 | export interface RpcHandler< 167 | TState extends Record, 168 | TReqStream extends boolean, 169 | TResStream extends boolean, 170 | TReq, 171 | TRes, 172 | > { 173 | (param: { 174 | req: RpcReq; 175 | getState: GetStateFn; 176 | stateChanges: StateChanges; 177 | teardown: TeardownFn; 178 | }): RpcRes; 179 | } 180 | 181 | type RpcReq = IsStream extends true 182 | ? AsyncGenerator 183 | : T; 184 | type RpcRes = IsStream extends true 185 | ? AsyncGenerator 186 | : Promise; 187 | -------------------------------------------------------------------------------- /src/react/useOnceEffect.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from "react"; 2 | 3 | /** 4 | * Guaranteed to be called only once in a component's lifecycle. 5 | * It called only once, even in strict mode. 6 | */ 7 | const useOnceEffect: typeof useEffect = (effect) => { 8 | const effectHasFiredRef = useRef(); 9 | useEffect(() => { 10 | if (effectHasFiredRef.current) return; 11 | else effectHasFiredRef.current = true; 12 | return effect(); 13 | }, []); 14 | }; 15 | 16 | export default useOnceEffect; 17 | -------------------------------------------------------------------------------- /src/react/useWrpClientImpl.ts: -------------------------------------------------------------------------------- 1 | import type { RpcClientImpl } from "https://deno.land/x/pbkit@v0.0.45/core/runtime/rpc.ts"; 2 | import { useEffect, useState } from "react"; 3 | import { WrpChannel } from "../channel.ts"; 4 | import { createWrpGuest } from "../guest.ts"; 5 | import { createWrpClientImpl } from "../rpc/client.ts"; 6 | 7 | /** 8 | * @deprecated use `@pbkit/wrp-jotai` instead 9 | */ 10 | export default function useWrpClientImpl( 11 | channel: WrpChannel | undefined, 12 | ): ReturnType | undefined { 13 | const [ 14 | clientImpl, 15 | setClientImpl, 16 | ] = useState(undefined); 17 | useEffect(() => { 18 | if (!channel) return; 19 | createWrpGuest({ channel }).then( 20 | (guest) => setClientImpl(() => createWrpClientImpl({ guest })), 21 | ); 22 | }, [channel]); 23 | return clientImpl; 24 | } 25 | -------------------------------------------------------------------------------- /src/react/useWrpIframeSocket.ts: -------------------------------------------------------------------------------- 1 | import { defer } from "https://deno.land/x/pbkit@v0.0.45/core/runtime/async/observer.ts"; 2 | import { Ref, useRef, useState } from "react"; 3 | import { Closer, Socket } from "../socket.ts"; 4 | import { createIframeSocket } from "../glue/iframe.ts"; 5 | import useOnceEffect from "./useOnceEffect.ts"; 6 | 7 | export interface UseWrpIframeSocketResult { 8 | iframeRef: Ref; 9 | socket: Socket | undefined; 10 | } 11 | export default function useWrpIframeSocket(): UseWrpIframeSocketResult { 12 | const iframeRef = useRef(null); 13 | const [socket, setSocket] = useState(undefined); 14 | useOnceEffect(() => { 15 | let socket: Closer & Socket; 16 | let unmounted = false; 17 | let waitForReconnect = defer(); 18 | const iframeElement = iframeRef.current!; 19 | (async () => { // reconnection loop 20 | while (true) { 21 | if (unmounted) return; 22 | try { 23 | socket = await createIframeSocket({ 24 | iframeElement, 25 | iframeOrigin: "*", 26 | onClosed: tryReconnect, 27 | }); 28 | setSocket(socket); 29 | await waitForReconnect; 30 | } catch { /* ignore handshake timeout */ } 31 | } 32 | })(); 33 | return () => { 34 | unmounted = true; 35 | tryReconnect(); 36 | }; 37 | function tryReconnect() { 38 | waitForReconnect.resolve(); 39 | waitForReconnect = defer(); 40 | } 41 | }); 42 | return { iframeRef, socket }; 43 | } 44 | -------------------------------------------------------------------------------- /src/react/useWrpParentSocket.ts: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { Socket } from "../socket.ts"; 3 | import useOnceEffect from "../react/useOnceEffect.ts"; 4 | import { subscribeParentSocket } from "../glue/parent.ts"; 5 | 6 | export interface UseWrpParentSocketResult { 7 | socket: Socket | undefined; 8 | error: Error | undefined; 9 | } 10 | /** 11 | * @deprecated use `@pbkit/wrp-jotai/parent` instead 12 | */ 13 | export default function useWrpParentSocket(): UseWrpParentSocketResult { 14 | const [socket, setSocket] = useState(undefined); 15 | const [error, setError] = useState(undefined); 16 | useOnceEffect(() => 17 | subscribeParentSocket((socket, error) => { 18 | setSocket(socket); 19 | setError(error); 20 | }) 21 | ); 22 | return { socket, error }; 23 | } 24 | -------------------------------------------------------------------------------- /src/rpc/client.ts: -------------------------------------------------------------------------------- 1 | import type { RpcClientImpl } from "https://deno.land/x/pbkit@v0.0.45/core/runtime/rpc.ts"; 2 | import type { WrpGuest } from "../guest.ts"; 3 | import type { LazyMetadata, Metadata } from "../metadata.ts"; 4 | import { getMethodName, mapAsyncGenerator } from "./misc.ts"; 5 | 6 | export interface CreateWrpClientImplConfig { 7 | guest: WrpGuest; 8 | metadata?: LazyMetadata; 9 | } 10 | 11 | export class WrpClientError extends Error { 12 | constructor(message: string) { 13 | super(message); 14 | } 15 | } 16 | 17 | export function createWrpClientImpl( 18 | config: CreateWrpClientImplConfig, 19 | ): RpcClientImpl { 20 | return (methodDescriptor) => { 21 | const { requestType, responseType } = methodDescriptor; 22 | const methodName = getMethodName(methodDescriptor); 23 | if (!config.guest.availableMethods.has(methodName)) { 24 | return () => { 25 | throw new WrpClientError("Method not available"); 26 | }; 27 | } 28 | return (req, metadata) => { 29 | const { res, header, trailer } = config.guest.request( 30 | methodName, 31 | mapAsyncGenerator(req, requestType.serializeBinary), 32 | { ...config.metadata, ...metadata }, 33 | ); 34 | const _res = mapAsyncGenerator(res, responseType.deserializeBinary); 35 | return [_res, header, trailer]; 36 | }; 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /src/rpc/misc.ts: -------------------------------------------------------------------------------- 1 | import type { MethodDescriptor } from "https://deno.land/x/pbkit@v0.0.45/core/runtime/rpc.ts"; 2 | 3 | export async function* mapAsyncGenerator( 4 | asyncGenerator: AsyncGenerator, 5 | fn: (value: T) => U, 6 | ): AsyncGenerator { 7 | for await (const value of asyncGenerator) yield fn(value); 8 | } 9 | 10 | export function getMethodName< 11 | TMethodName extends string, 12 | TServiceName extends string, 13 | TDescriptor extends MethodDescriptor, 14 | >(descriptor: TDescriptor): `${TServiceName}/${TMethodName}` { 15 | return `${descriptor.service.serviceName}/${descriptor.methodName}`; 16 | } 17 | -------------------------------------------------------------------------------- /src/rpc/server.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createServerImplBuilder, 3 | Method, 4 | } from "https://deno.land/x/pbkit@v0.0.45/core/runtime/rpc.ts"; 5 | import type { WrpHost, WrpRequest } from "../host.ts"; 6 | import type { Metadata } from "../metadata.ts"; 7 | import { getMethodName, mapAsyncGenerator } from "./misc.ts"; 8 | 9 | export interface CreateWrpServerConfig { 10 | host: WrpHost; 11 | methods: AsyncGenerator>; 12 | } 13 | export async function createWrpServer(config: CreateWrpServerConfig) { 14 | const methods: { 15 | [methodName: string]: Method; 16 | } = {}; 17 | for await (const method of config.methods) { 18 | const methodName = getMethodName(method[0]); 19 | methods[methodName] = method; 20 | } 21 | return { 22 | async listen() { 23 | for await (const request of config.host.listen()) { 24 | const method = methods[request.methodName]; 25 | const sendHeader = doOnce(request.sendHeader); 26 | const sendTrailer = doOnce(request.sendTrailer); 27 | if (!method) { 28 | sendHeader({}); 29 | sendTrailer({ 30 | "wrp-status": "error", 31 | "wrp-message": `Method not found: ${request.methodName}`, 32 | }); 33 | continue; 34 | } 35 | const [methodDescriptor, methodImpl] = method; 36 | const { req } = request; 37 | const { requestType, responseType } = methodDescriptor; 38 | const [res, header, trailer] = methodImpl( 39 | mapAsyncGenerator(req, requestType.deserializeBinary), 40 | request, 41 | ); 42 | (async function () { 43 | let _header: Metadata = {}; 44 | try { 45 | sendHeader(_header = await header); 46 | for await (const value of res) { 47 | request.sendPayload(responseType.serializeBinary(value)); 48 | } 49 | sendTrailer(await trailer); 50 | } catch (err) { 51 | sendHeader(_header); 52 | sendTrailer({ 53 | "wrp-status": "error", 54 | "wrp-message": err?.message ?? "", 55 | }); 56 | } 57 | })(); 58 | } 59 | }, 60 | }; 61 | } 62 | 63 | export function createWrpServerImplBuilder() { 64 | return createServerImplBuilder(); 65 | } 66 | 67 | function doOnce any>(fn: T) { 68 | let flag = false; 69 | return (...args: Parameters) => { 70 | if (flag) return; 71 | flag = true; 72 | return fn.apply(null, args); 73 | }; 74 | } 75 | -------------------------------------------------------------------------------- /src/socket.ts: -------------------------------------------------------------------------------- 1 | export type Socket = Reader & Writer; 2 | 3 | // same as `Deno.Reader` 4 | export interface Reader { 5 | read(p: Uint8Array): Promise; 6 | } 7 | 8 | // same as `Deno.Writer` 9 | export interface Writer { 10 | write(p: Uint8Array): Promise; 11 | } 12 | 13 | // same as `Deno.Closer` 14 | export interface Closer { 15 | close(): void; 16 | } 17 | -------------------------------------------------------------------------------- /src/tee.ts: -------------------------------------------------------------------------------- 1 | import { Type as WrpMessage } from "./generated/messages/pbkit/wrp/WrpMessage.ts"; 2 | import { WrpChannel } from "./channel.ts"; 3 | import { createWrpGuest, WrpGuest } from "./guest.ts"; 4 | 5 | export interface ChannelAndGuest { 6 | channel: WrpChannel; 7 | guest: Promise; 8 | } 9 | export default function tee(sourceChannel: WrpChannel): ChannelAndGuest { 10 | const listeners: ((message?: WrpMessage) => void)[] = []; 11 | const guest = createWrpGuest({ 12 | channel: { 13 | ...sourceChannel, 14 | async *listen() { 15 | for await (const message of sourceChannel.listen()) { 16 | yield message; 17 | for (const listener of listeners) listener(message); 18 | listeners.length = 0; 19 | } 20 | }, 21 | }, 22 | }); 23 | const channel: WrpChannel = { 24 | ...sourceChannel, 25 | async *listen() { 26 | while (true) { 27 | const message = await new Promise( 28 | (resolve) => listeners.push(resolve), 29 | ); 30 | if (!message) break; 31 | yield message; 32 | } 33 | }, 34 | }; 35 | return { channel, guest }; 36 | } 37 | -------------------------------------------------------------------------------- /src/wrp.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package pbkit.wrp; 3 | 4 | message WrpMessage { 5 | oneof message { 6 | WrpHostMessage_Initialize Host_Initialize = 1; 7 | WrpHostMessage_Error Host_Error = 2; 8 | WrpHostMessage_ResStart Host_ResStart = 3; 9 | WrpHostMessage_ResPayload Host_ResPayload = 4; 10 | WrpHostMessage_ResFinish Host_ResFinish = 5; 11 | WrpGuestMessage_ReqStart Guest_ReqStart = 6; 12 | WrpGuestMessage_ReqPayload Guest_ReqPayload = 7; 13 | WrpGuestMessage_ReqFinish Guest_ReqFinish = 8; 14 | WrpGuestMessage_ResFinish Guest_ResFinish = 9; 15 | } 16 | } 17 | 18 | message WrpHostMessage_Initialize { 19 | repeated string available_methods = 1; 20 | } 21 | message WrpHostMessage_Error { 22 | string message = 1; 23 | } 24 | message WrpHostMessage_ResStart { 25 | string req_id = 1; 26 | map header = 2; 27 | } 28 | message WrpHostMessage_ResPayload { 29 | string req_id = 1; 30 | bytes payload = 2; 31 | } 32 | message WrpHostMessage_ResFinish { 33 | string req_id = 1; 34 | map trailer = 2; 35 | } 36 | 37 | message WrpGuestMessage_ReqStart { 38 | string req_id = 1; 39 | string method_name = 2; 40 | map metadata = 3; 41 | } 42 | message WrpGuestMessage_ReqPayload { 43 | string req_id = 1; 44 | bytes payload = 2; 45 | } 46 | message WrpGuestMessage_ReqFinish { 47 | string req_id = 1; 48 | } 49 | message WrpGuestMessage_ResFinish { 50 | string req_id = 1; 51 | } 52 | --------------------------------------------------------------------------------