├── .github └── workflows │ ├── build&test.yml │ └── static.yml ├── .gitignore ├── LICENSE ├── README.md ├── docs ├── .gitignore ├── package.json └── src │ ├── .vuepress │ ├── config.ts │ ├── navbar │ │ ├── en.ts │ │ ├── index.ts │ │ └── zh.ts │ ├── public │ │ ├── logo.png │ │ └── logo.svg │ ├── styles │ │ ├── config.scss │ │ ├── index.scss │ │ └── palette.scss │ └── theme.ts │ ├── README.md │ ├── advance │ ├── README.md │ ├── channel.md │ ├── context.md │ ├── customCodec.md │ ├── customLibp2pNode.md │ ├── errorCatch.md │ ├── middleware.md │ ├── plugin.md │ └── service.md │ ├── guide │ ├── README.md │ ├── client.md │ └── server.md │ └── zh │ ├── .gitkeep │ └── README.md ├── examples ├── example-basic │ ├── index.ts │ ├── package.json │ ├── server.ts │ └── tsconfig.json ├── example-react │ ├── .gitignore │ ├── index.html │ ├── package.json │ ├── public │ │ └── vite.svg │ ├── src │ │ ├── App.css │ │ ├── App.tsx │ │ ├── assets │ │ │ └── react.svg │ │ ├── index.css │ │ ├── main.tsx │ │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── example-svelte │ ├── .gitignore │ ├── README.md │ ├── __unconfig_vite.config.ts │ ├── index.html │ ├── package.json │ ├── public │ │ └── vite.svg │ ├── src │ │ ├── App.svelte │ │ ├── app.css │ │ ├── assets │ │ │ └── svelte.svg │ │ ├── lib │ │ │ └── Counter.svelte │ │ ├── main.ts │ │ └── vite-env.d.ts │ ├── svelte.config.js │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── example-vue-customAddr │ ├── .gitignore │ ├── README.md │ ├── index.html │ ├── package.json │ ├── public │ │ └── vite.svg │ ├── server.ts │ ├── src │ │ ├── App.vue │ │ ├── assets │ │ │ └── vue.svg │ │ ├── client.ts │ │ ├── components │ │ │ └── HelloWorld.vue │ │ ├── main.ts │ │ ├── style.css │ │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts └── example-vue │ ├── .gitignore │ ├── README.md │ ├── __unconfig_vite.config.ts │ ├── index.html │ ├── package.json │ ├── public │ └── vite.svg │ ├── src │ ├── App.vue │ ├── assets │ │ └── vue.svg │ ├── components │ │ └── HelloWorld.vue │ ├── main.ts │ ├── style.css │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── package.json ├── packages ├── libp2p-transport │ ├── README.md │ ├── package.json │ ├── src │ │ ├── client.ts │ │ ├── codec.ts │ │ ├── common.ts │ │ ├── const.ts │ │ ├── error.ts │ │ ├── index.ts │ │ ├── logger.ts │ │ ├── server.ts │ │ ├── types.ts │ │ └── utils.ts │ └── test │ │ ├── handler.ts │ │ ├── http.ts │ │ ├── index.bench.ts │ │ ├── index.test.ts │ │ ├── jsonCodec.ts │ │ ├── node.ts │ │ └── types.ts └── metapoint │ ├── README.md │ ├── package.json │ ├── src │ ├── helper.ts │ ├── index.ts │ ├── node.ts │ └── types.ts │ ├── test │ └── index.test.ts │ └── vite.config.ts ├── pnpm-workspace.yaml ├── renovate.json └── tsconfig.json /.github/workflows/build&test.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | pull_request: 6 | 7 | jobs: 8 | cache-and-install: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v3 14 | 15 | - name: Install Node.js 16 | uses: actions/setup-node@v3 17 | with: 18 | node-version: 19 19 | 20 | - uses: pnpm/action-setup@v2 21 | name: Install pnpm 22 | id: pnpm-install 23 | with: 24 | version: 7 25 | run_install: false 26 | 27 | - name: Get pnpm store directory 28 | id: pnpm-cache 29 | shell: bash 30 | run: | 31 | echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT 32 | 33 | - uses: actions/cache@v3 34 | name: Setup pnpm cache 35 | with: 36 | path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} 37 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 38 | restore-keys: | 39 | ${{ runner.os }}-pnpm-store- 40 | 41 | - name: Install dependencies 42 | run: pnpm install 43 | 44 | - name: Build 45 | run: pnpm -r build 46 | 47 | - name: Test 48 | run: pnpm -r test 49 | -------------------------------------------------------------------------------- /.github/workflows/static.yml: -------------------------------------------------------------------------------- 1 | # Simple workflow for deploying static content to GitHub Pages 2 | name: Deploy static content to Pages 3 | 4 | on: 5 | # Runs on pushes targeting the default branch 6 | push: 7 | branches: ["master"] 8 | 9 | # Allows you to run this workflow manually from the Actions tab 10 | workflow_dispatch: 11 | 12 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 13 | permissions: 14 | contents: read 15 | pages: write 16 | id-token: write 17 | 18 | # Allow one concurrent deployment 19 | concurrency: 20 | group: "pages" 21 | cancel-in-progress: true 22 | 23 | jobs: 24 | # Single deploy job since we're just deploying 25 | deploy: 26 | environment: 27 | name: github-pages 28 | url: ${{ steps.deployment.outputs.page_url }} 29 | runs-on: ubuntu-latest 30 | steps: 31 | - name: Checkout 32 | uses: actions/checkout@v3 33 | 34 | - name: Setup Pages 35 | uses: actions/configure-pages@v3 36 | 37 | - name: Install Node.js 38 | uses: actions/setup-node@v3 39 | with: 40 | node-version: 19 41 | 42 | - uses: pnpm/action-setup@v2 43 | name: Install pnpm 44 | id: pnpm-install 45 | with: 46 | version: 7 47 | run_install: false 48 | 49 | - name: Get pnpm store directory 50 | id: pnpm-cache 51 | shell: bash 52 | run: | 53 | echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT 54 | 55 | - uses: actions/cache@v3 56 | name: Setup pnpm cache 57 | with: 58 | path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} 59 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 60 | restore-keys: | 61 | ${{ runner.os }}-pnpm-store- 62 | 63 | - name: Install dependencies 64 | run: pnpm install 65 | 66 | - name: Build Pages 67 | run: cd docs && export NODE_OPTIONS=--max_old_space_size=4096 && pnpm build 68 | 69 | - name: Upload artifact 70 | uses: actions/upload-pages-artifact@v1 71 | with: 72 | # Upload entire repository 73 | path: './docs/src/.vuepress/dist' 74 | 75 | - name: Deploy to GitHub Pages 76 | id: deployment 77 | uses: actions/deploy-pages@v2 78 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | pnpm-lock.yaml 2 | .vscode 3 | node_modules 4 | dist -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 MetaPoint Authors. All Rights Reserved 2 | 3 | Apache License 4 | Version 2.0, January 2004 5 | http://www.apache.org/licenses/ 6 | 7 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 8 | 9 | 1. Definitions. 10 | 11 | "License" shall mean the terms and conditions for use, reproduction, 12 | and distribution as defined by Sections 1 through 9 of this document. 13 | 14 | "Licensor" shall mean the copyright owner or entity authorized by 15 | the copyright owner that is granting the License. 16 | 17 | "Legal Entity" shall mean the union of the acting entity and all 18 | other entities that control, are controlled by, or are under common 19 | control with that entity. For the purposes of this definition, 20 | "control" means (i) the power, direct or indirect, to cause the 21 | direction or management of such entity, whether by contract or 22 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 23 | outstanding shares, or (iii) beneficial ownership of such entity. 24 | 25 | "You" (or "Your") shall mean an individual or Legal Entity 26 | exercising permissions granted by this License. 27 | 28 | "Source" form shall mean the preferred form for making modifications, 29 | including but not limited to software source code, documentation 30 | source, and configuration files. 31 | 32 | "Object" form shall mean any form resulting from mechanical 33 | transformation or translation of a Source form, including but 34 | not limited to compiled object code, generated documentation, 35 | and conversions to other media types. 36 | 37 | "Work" shall mean the work of authorship, whether in Source or 38 | Object form, made available under the License, as indicated by a 39 | copyright notice that is included in or attached to the work 40 | (an example is provided in the Appendix below). 41 | 42 | "Derivative Works" shall mean any work, whether in Source or Object 43 | form, that is based on (or derived from) the Work and for which the 44 | editorial revisions, annotations, elaborations, or other modifications 45 | represent, as a whole, an original work of authorship. For the purposes 46 | of this License, Derivative Works shall not include works that remain 47 | separable from, or merely link (or bind by name) to the interfaces of, 48 | the Work and Derivative Works thereof. 49 | 50 | "Contribution" shall mean any work of authorship, including 51 | the original version of the Work and any modifications or additions 52 | to that Work or Derivative Works thereof, that is intentionally 53 | submitted to Licensor for inclusion in the Work by the copyright owner 54 | or by an individual or Legal Entity authorized to submit on behalf of 55 | the copyright owner. For the purposes of this definition, "submitted" 56 | means any form of electronic, verbal, or written communication sent 57 | to the Licensor or its representatives, including but not limited to 58 | communication on electronic mailing lists, source code control systems, 59 | and issue tracking systems that are managed by, or on behalf of, the 60 | Licensor for the purpose of discussing and improving the Work, but 61 | excluding communication that is conspicuously marked or otherwise 62 | designated in writing by the copyright owner as "Not a Contribution." 63 | 64 | "Contributor" shall mean Licensor and any individual or Legal Entity 65 | on behalf of whom a Contribution has been received by Licensor and 66 | subsequently incorporated within the Work. 67 | 68 | 2. Grant of Copyright License. Subject to the terms and conditions of 69 | this License, each Contributor hereby grants to You a perpetual, 70 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 71 | copyright license to reproduce, prepare Derivative Works of, 72 | publicly display, publicly perform, sublicense, and distribute the 73 | Work and such Derivative Works in Source or Object form. 74 | 75 | 3. Grant of Patent License. Subject to the terms and conditions of 76 | this License, each Contributor hereby grants to You a perpetual, 77 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 78 | (except as stated in this section) patent license to make, have made, 79 | use, offer to sell, sell, import, and otherwise transfer the Work, 80 | where such license applies only to those patent claims licensable 81 | by such Contributor that are necessarily infringed by their 82 | Contribution(s) alone or by combination of their Contribution(s) 83 | with the Work to which such Contribution(s) was submitted. If You 84 | institute patent litigation against any entity (including a 85 | cross-claim or counterclaim in a lawsuit) alleging that the Work 86 | or a Contribution incorporated within the Work constitutes direct 87 | or contributory patent infringement, then any patent licenses 88 | granted to You under this License for that Work shall terminate 89 | as of the date such litigation is filed. 90 | 91 | 4. Redistribution. You may reproduce and distribute copies of the 92 | Work or Derivative Works thereof in any medium, with or without 93 | modifications, and in Source or Object form, provided that You 94 | meet the following conditions: 95 | 96 | (a) You must give any other recipients of the Work or 97 | Derivative Works a copy of this License; and 98 | 99 | (b) You must cause any modified files to carry prominent notices 100 | stating that You changed the files; and 101 | 102 | (c) You must retain, in the Source form of any Derivative Works 103 | that You distribute, all copyright, patent, trademark, and 104 | attribution notices from the Source form of the Work, 105 | excluding those notices that do not pertain to any part of 106 | the Derivative Works; and 107 | 108 | (d) If the Work includes a "NOTICE" text file as part of its 109 | distribution, then any Derivative Works that You distribute must 110 | include a readable copy of the attribution notices contained 111 | within such NOTICE file, excluding those notices that do not 112 | pertain to any part of the Derivative Works, in at least one 113 | of the following places: within a NOTICE text file distributed 114 | as part of the Derivative Works; within the Source form or 115 | documentation, if provided along with the Derivative Works; or, 116 | within a display generated by the Derivative Works, if and 117 | wherever such third-party notices normally appear. The contents 118 | of the NOTICE file are for informational purposes only and 119 | do not modify the License. You may add Your own attribution 120 | notices within Derivative Works that You distribute, alongside 121 | or as an addendum to the NOTICE text from the Work, provided 122 | that such additional attribution notices cannot be construed 123 | as modifying the License. 124 | 125 | You may add Your own copyright statement to Your modifications and 126 | may provide additional or different license terms and conditions 127 | for use, reproduction, or distribution of Your modifications, or 128 | for any such Derivative Works as a whole, provided Your use, 129 | reproduction, and distribution of the Work otherwise complies with 130 | the conditions stated in this License. 131 | 132 | 5. Submission of Contributions. Unless You explicitly state otherwise, 133 | any Contribution intentionally submitted for inclusion in the Work 134 | by You to the Licensor shall be under the terms and conditions of 135 | this License, without any additional terms or conditions. 136 | Notwithstanding the above, nothing herein shall supersede or modify 137 | the terms of any separate license agreement you may have executed 138 | with Licensor regarding such Contributions. 139 | 140 | 6. Trademarks. This License does not grant permission to use the trade 141 | names, trademarks, service marks, or product names of the Licensor, 142 | except as required for reasonable and customary use in describing the 143 | origin of the Work and reproducing the content of the NOTICE file. 144 | 145 | 7. Disclaimer of Warranty. Unless required by applicable law or 146 | agreed to in writing, Licensor provides the Work (and each 147 | Contributor provides its Contributions) on an "AS IS" BASIS, 148 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 149 | implied, including, without limitation, any warranties or conditions 150 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 151 | PARTICULAR PURPOSE. You are solely responsible for determining the 152 | appropriateness of using or redistributing the Work and assume any 153 | risks associated with Your exercise of permissions under this License. 154 | 155 | 8. Limitation of Liability. In no event and under no legal theory, 156 | whether in tort (including negligence), contract, or otherwise, 157 | unless required by applicable law (such as deliberate and grossly 158 | negligent acts) or agreed to in writing, shall any Contributor be 159 | liable to You for damages, including any direct, indirect, special, 160 | incidental, or consequential damages of any character arising as a 161 | result of this License or out of the use or inability to use the 162 | Work (including but not limited to damages for loss of goodwill, 163 | work stoppage, computer failure or malfunction, or any and all 164 | other commercial damages or losses), even if such Contributor 165 | has been advised of the possibility of such damages. 166 | 167 | 9. Accepting Warranty or Additional Liability. While redistributing 168 | the Work or Derivative Works thereof, You may choose to offer, 169 | and charge a fee for, acceptance of support, warranty, indemnity, 170 | or other liability obligations and/or rights consistent with this 171 | License. However, in accepting such obligations, You may act only 172 | on Your own behalf and on Your sole responsibility, not on behalf 173 | of any other Contributor, and only if You agree to indemnify, 174 | defend, and hold each Contributor harmless for any liability 175 | incurred by, or claims asserted against, such Contributor by reason 176 | of your accepting any such warranty or additional liability. 177 | 178 | END OF TERMS AND CONDITIONS 179 | 180 | APPENDIX: How to apply the Apache License to your work. 181 | 182 | To apply the Apache License to your work, attach the following 183 | boilerplate notice, with the fields enclosed by brackets "[]" 184 | replaced with your own identifying information. (Don't include 185 | the brackets!) The text should be enclosed in the appropriate 186 | comment syntax for the file format. We also recommend that a 187 | file or class name and description of purpose be included on the 188 | same "printed page" as the copyright notice for easier 189 | identification within third-party archives. 190 | 191 | Copyright (c) 2023 MetaPoint Authors. All Rights Reserved. 192 | 193 | Licensed under the Apache License, Version 2.0 (the "License"); 194 | you may not use this file except in compliance with the License. 195 | You may obtain a copy of the License at 196 | 197 | http://www.apache.org/licenses/LICENSE-2.0 198 | 199 | Unless required by applicable law or agreed to in writing, software 200 | distributed under the License is distributed on an "AS IS" BASIS, 201 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 202 | See the License for the specific language governing permissions and 203 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | metapoint 6 | 7 | 8 |
9 | 10 |
11 |

MetaPoint

12 |

Meta first and low-code.
Peer-to-Peer typesafe APIs or Channels made easy.

13 | 14 | Apache-2.0 License 15 | 16 | 17 | Discord 18 | 19 | 20 | Version 21 | 22 | 23 | Downloads 24 | 25 | 26 | Total Downloads 27 | 28 |
29 |
30 | Demo 31 |
32 |

33 | MetaPoint lets you focus on what you want to do 34 |

35 |
36 |
37 |
38 | 39 | ## Intro 40 | 41 | More information at https://metapointtech.github.io/metapoint/ 42 | 43 | ## Quickstart 44 | 45 | ```typescript 46 | import { h, MetaType, peer, z } from "metapoint"; 47 | 48 | // router group 49 | const g = h(); 50 | 51 | // node1(nodejs/browser) 52 | const node1 = await peer({ 53 | endpoint: { 54 | numberAdd: g.handler({ 55 | func: async ({ data, send, done }) => { 56 | await send(data + 1); 57 | await done(); 58 | }, 59 | input: z.number(), 60 | output: z.number(), 61 | }), 62 | }, 63 | }); 64 | export type meta = MetaType; 65 | 66 | // node2(nodejs/browser) 67 | const node2 = await peer(); 68 | const channel = await node2.connect(node1.meta().addrs); 69 | const add = await channel("numberAdd"); 70 | console.log(await add(1)); // [2] 71 | ``` 72 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | node_modules/ 3 | src/.vuepress/.cache/ 4 | src/.vuepress/.temp/ 5 | src/.vuepress/dist/ 6 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "metapoint-doc", 3 | "version": "1.0.0", 4 | "description": "⚡Meta first and low-code. Peer-to-Peer typesafe APIs or Channels made easy.", 5 | "license": "Apache-2.0", 6 | "type": "module", 7 | "scripts": { 8 | "build": "vuepress-vite build src --clean-cache", 9 | "clean-dev": "vuepress-vite dev src --clean-cache", 10 | "dev": "vuepress-vite dev src", 11 | "update-package": "pnpm dlx vp-update" 12 | }, 13 | "devDependencies": { 14 | "@vue/repl": "^2.6.3", 15 | "@vuepress/cli": "2.0.0-beta.67", 16 | "@vuepress/client": "2.0.0-beta.67", 17 | "vue": "^3.3.8", 18 | "vuepress": "2.0.0-beta.67", 19 | "vuepress-theme-hope": "2.0.0-beta.246", 20 | "vuepress-vite": "2.0.0-beta.67" 21 | } 22 | } -------------------------------------------------------------------------------- /docs/src/.vuepress/config.ts: -------------------------------------------------------------------------------- 1 | import { defineUserConfig } from "vuepress"; 2 | import theme from "./theme.js"; 3 | 4 | export default defineUserConfig({ 5 | base: "/metapoint/", 6 | locales: { 7 | "/": { 8 | lang: "en-US", 9 | title: "MetaPoint", 10 | description: "Low-code p2p web framework", 11 | }, 12 | "/zh/": { 13 | lang: "zh-CN", 14 | title: "元点", 15 | description: "低代码点对点通讯框架", 16 | }, 17 | }, 18 | theme, 19 | }); 20 | -------------------------------------------------------------------------------- /docs/src/.vuepress/navbar/en.ts: -------------------------------------------------------------------------------- 1 | import { navbar } from "vuepress-theme-hope"; 2 | 3 | export const enNavbar = navbar([ 4 | "/", 5 | { 6 | text: "Guide", 7 | icon: "creative", 8 | link: "/guide/", 9 | }, 10 | ]); 11 | -------------------------------------------------------------------------------- /docs/src/.vuepress/navbar/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./en.js"; 2 | export * from "./zh.js"; 3 | -------------------------------------------------------------------------------- /docs/src/.vuepress/navbar/zh.ts: -------------------------------------------------------------------------------- 1 | import { navbar } from "vuepress-theme-hope"; 2 | 3 | export const zhNavbar = navbar([ 4 | "/zh/", 5 | { 6 | text: "指南", 7 | icon: "creative", 8 | link: "/zh/guide/", 9 | }, 10 | ]); 11 | -------------------------------------------------------------------------------- /docs/src/.vuepress/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MetaPointTech/metapoint/09b1f5976a1345b9c9db945a0ba8f771e3e35b4e/docs/src/.vuepress/public/logo.png -------------------------------------------------------------------------------- /docs/src/.vuepress/public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /docs/src/.vuepress/styles/config.scss: -------------------------------------------------------------------------------- 1 | // you can change config here 2 | $colors: #c0392b, #d35400, #f39c12, #27ae60, #16a085, #2980b9, #8e44ad, #2c3e50, 3 | #7f8c8d !default; 4 | -------------------------------------------------------------------------------- /docs/src/.vuepress/styles/index.scss: -------------------------------------------------------------------------------- 1 | // place your custom styles here 2 | -------------------------------------------------------------------------------- /docs/src/.vuepress/styles/palette.scss: -------------------------------------------------------------------------------- 1 | // you can change colors here 2 | $theme-color: #096dd9; 3 | -------------------------------------------------------------------------------- /docs/src/.vuepress/theme.ts: -------------------------------------------------------------------------------- 1 | import { hopeTheme } from "vuepress-theme-hope"; 2 | import { enNavbar, zhNavbar } from "./navbar/index.js"; 3 | 4 | export default hopeTheme({ 5 | hostname: "https://vuepress-theme-hope-docs-demo.netlify.app", 6 | favicon: "/logo.svg", 7 | author: { 8 | name: "SOVLOOKUP", 9 | url: "https://github.com/SOVLOOKUP", 10 | }, 11 | iconAssets: "iconfont", 12 | logo: "/logo.svg", 13 | repo: "SOVLOOKUP/metapoint", 14 | docsDir: "demo/theme-docs/src", 15 | displayFooter: false, 16 | locales: { 17 | "/": { 18 | // navbar 19 | navbar: enNavbar, 20 | footer: "Default footer", 21 | metaLocales: { 22 | editLink: "Edit this page on GitHub", 23 | }, 24 | }, 25 | /** 26 | * Chinese locale config 27 | */ 28 | "/zh/": { 29 | // navbar 30 | navbar: zhNavbar, 31 | footer: "默认页脚", 32 | // page meta 33 | metaLocales: { 34 | editLink: "在 GitHub 上编辑此页", 35 | }, 36 | }, 37 | }, 38 | plugins: { 39 | seo: true, 40 | comment: { 41 | comment: true, 42 | provider: "Giscus", 43 | repo: `SOVLOOKUP/metapoint`, 44 | repoId: "R_kgDOI1te7w", 45 | category: "Comments", 46 | categoryId: "DIC_kwDOI1te784CUkvI", 47 | lazyLoading: true, 48 | reactionsEnabled: true, 49 | lightTheme: "preferred_color_scheme", 50 | }, 51 | // all features are enabled for demo, only preserve features you need here 52 | mdEnhance: { 53 | align: true, 54 | attrs: true, 55 | codetabs: true, 56 | container: true, 57 | demo: true, 58 | figure: true, 59 | imgLazyload: true, 60 | imgSize: true, 61 | include: true, 62 | mark: true, 63 | playground: { 64 | presets: ["ts", "vue"], 65 | }, 66 | stylize: [ 67 | { 68 | matcher: "Recommended", 69 | replacer: ({ tag }) => { 70 | if (tag === "em") { 71 | return { 72 | tag: "Badge", 73 | attrs: { type: "tip" }, 74 | content: "Recommended", 75 | }; 76 | } 77 | }, 78 | }, 79 | ], 80 | sub: true, 81 | sup: true, 82 | tabs: true, 83 | vPre: true, 84 | vuePlayground: true, 85 | }, 86 | }, 87 | }); 88 | -------------------------------------------------------------------------------- /docs/src/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | home: true 3 | icon: home 4 | title: MetaPoint 5 | heroImage: /logo.png 6 | heroText: MetaPoint 7 | tagline: ⚡Meta first and low-code. Peer-to-Peer typesafe APIs or Channels made easy. 8 | actions: 9 | - text: Guide 💡 10 | link: /guide/ 11 | type: primary 12 | 13 | - text: ⭐ Star 14 | link: https://github.com/SOVLOOKUP/metapoint/stargazers 15 | 16 | features: 17 | - title: ⚡Efficient 18 | details: HTTP frequent connections waste your time? MetaPoint connect only once, communicate unlimited times. 19 | 20 | - title: 🤝Bidirectional channel 21 | details: MetaPoint establish a bidirectional channel between devices, so that you no longer have to worry about server-side push. 22 | 23 | - title: 🪞Shadow functions 24 | details: Call APIs painlessly, just like using local functions(Even includes error catching). 25 | 26 | - title: 👌Autocompletion 27 | details: Just like calling native functions, TypeScript gives you autocompletion across devices! 28 | 29 | - title: 🎡Run everywhere 30 | details: Nodejs and Browser? Metapoint can all run! 31 | 32 | - title: 🪐P2P or C/S 33 | details: Whether there is a centralized server or not, you have the final say. 34 | 35 | - title: 🔢Data codec agnostic 36 | details: JSON, Protobuff, xml, Thrift, MessagePack ... 37 | 38 | - title: 📡Transport protocol agnostic 39 | details: tcp, ws, webtransport, quic... 40 | 41 | - title: 🔒Automatic typesafety 42 | details: Something changed? TypeScript will warn you of errors before you even save the file. 43 | 44 | - title: ⚙Auto management 45 | details: Streams are managed by MetaPoint. You only need to consider the business code! 46 | 47 | - title: 🔐Secure transmission 48 | details: Devices communicate with each other over encrypted channels. 49 | 50 | - title: 🧩Plugin support 51 | details: Extend MetaPoint through plugins. 52 | 53 | copyright: Copyright © 2023 SOVLOOKUP 54 | footer: Apache-2.0 Licensed 55 | --- 56 | 57 | ## ❓️What's MetaPoint? 58 | 59 |
60 | Demo 61 |
62 |

63 | MetaPoint lets you focus on what you want to do 64 |

65 |
66 |
67 | 68 | - You _**no longer**_ need to pay attention to tedious network layer coding, 69 | just write business code. Save a lot of time and energy. 70 | 71 | - MetaPoint works well with nodejs and _**any**_ front-end framework. 72 | 73 | - MetaPoint saves your team _**a lot of communication time**_ because the SDK is 74 | _**automatically**_ generated. API documentation is also no longer needed. 75 | 76 | - You will _**never**_ get your API called incorrectly. TypeScript will warn you 77 | of errors in the call before you even save the file. 78 | 79 | - Thanks to the extensibility of libp2p, you can use _**any**_ transport 80 | protocol, including any in the _**future**_, which means _**zero**_ upgrade 81 | cost. 82 | 83 | - MetaPoint is great for making _**real-time**_ applications. 84 | 85 | - MetaPoint is great for making _**client-first**_ apps. 86 | 87 | - Using metapoints makes your app to be _**more immune to malicious crawlers**_. 88 | 89 | - Metapoint's _**secure transmission**_ protects you from man-in-the-middle 90 | attacks. 91 | 92 | ## 🛠Install 93 | 94 | Install MetaPoint: 95 | 96 | ::: code-tabs#shell 97 | 98 | @tab:active pnpm 99 | 100 | ```bash 101 | pnpm add metapoint 102 | ``` 103 | 104 | @tab yarn 105 | 106 | ```bash 107 | yarn add metapoint 108 | ``` 109 | 110 | @tab npm 111 | 112 | ```bash 113 | npm i metapoint 114 | ``` 115 | 116 | ::: 117 | 118 | ## 🚀Usage 119 | 120 | 👉 Define endpoints 121 | 122 | ```ts {7-14} 123 | // server.ts 124 | import { h, MetaType, peer, z } from "metapoint"; 125 | 126 | const group = h({ context: { addnum: 1 } }); 127 | 128 | const endpoint = { 129 | add: group.handler({ 130 | func: async ({ data, send, done, context }) => { 131 | await send(data + context.addnum); 132 | await done(); 133 | }, 134 | input: z.number(), 135 | output: z.number(), 136 | }), 137 | }; 138 | 139 | const node = await peer({ endpoint }); 140 | export type Meta = MetaType; 141 | 142 | console.log("MetaPoint addr: ", node.meta().addrs); 143 | // /ipv4/127.0.0.1/xxxxxx (it's your server's connect addr) 144 | ``` 145 | 146 | 👉 Call endpoints 147 | 148 | ```ts {6-7} 149 | // client.ts 150 | import { peer } from "metapoint"; 151 | import type { Meta } from "./server"; 152 | const node = await peer(); 153 | const channel = await node.connect("your server addr"); 154 | const add = await channel("add"); 155 | console.log(await add(1)); // [2] 156 | ``` 157 | 158 | ## 🎉Try it out for yourself! 159 | 160 |
161 | 162 |
163 | -------------------------------------------------------------------------------- /docs/src/advance/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Advanced Usage 3 | index: true 4 | order: 1 5 | icon: note 6 | category: 7 | - Guide 8 | --- 9 | 10 | Here is advanced usage of MetaPoint. 11 | 12 | If you are new to MetaPoint, we recommend that you read the 13 | [guide](../guide/README.md) first. 14 | -------------------------------------------------------------------------------- /docs/src/advance/channel.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Channel 3 | index: true 4 | order: 2 5 | icon: emmet 6 | category: 7 | - Guide 8 | --- 9 | 10 | This is the "function style" code we wrote earlier in 11 | [guide](../guide/client.md). 12 | 13 | ```ts {5-6} 14 | import { peer } from "metapoint"; 15 | import type { Meta } from "./server"; 16 | const node = await peer(); 17 | const channel = await node.connect("your server addr"); 18 | const helloworld = await channel("helloworld"); 19 | console.log(await helloworld("sovlookup")); // ["hi sovlookup", "hello world!"] 20 | ``` 21 | 22 | You can also write in the "chennel style": 23 | 24 | ```ts {6-8} 25 | import { peer } from "metapoint"; 26 | import type { Meta } from "./server"; 27 | const node = await peer(); 28 | const channel = await node.connect("your server addr"); 29 | const helloworld = await channel("helloworld"); 30 | for await (const msg of helloworld) { 31 | console.log(msg); // "hi sovlookup", "hello world!" 32 | } 33 | ``` 34 | 35 | This is useful for receiving response from endpoints that send multiple returns 36 | or infinite returns. 37 | 38 | It is recommended to use the "function style" for one return, and use the 39 | "channel style" for multiple returns or infinite returns. 40 | -------------------------------------------------------------------------------- /docs/src/advance/context.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Context 3 | index: true 4 | order: 3 5 | icon: float 6 | category: 7 | - Guide 8 | --- 9 | 10 | You can define context that are shared in the endpoint group, and you can read 11 | or modify the context in endpoint handlers. 12 | 13 | ```ts {3,8} 14 | import { h, MetaType, peer, z } from "metapoint"; 15 | 16 | const group = h({ context: { world: "world" } }); 17 | 18 | const endpoint = { 19 | helloworld: group.handler({ 20 | func: async ({ data, send, done, context }) => { 21 | await send("hello " + context.world + "!"); 22 | await send("hi " + data); 23 | await done(); 24 | }, 25 | input: z.string(), 26 | output: z.string(), 27 | }), 28 | }; 29 | 30 | const node = await peer({ endpoint }); 31 | 32 | export type Meta = MetaType; 33 | console.log("MetaPoint addr: ", node.meta().addrs); 34 | // /ipv4/127.0.0.1/xxxxxx (it's your server's connect addr) 35 | ``` 36 | -------------------------------------------------------------------------------- /docs/src/advance/customCodec.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Custom Codec 3 | index: true 4 | order: 8 5 | icon: json 6 | category: 7 | - Guide 8 | --- 9 | 10 | You can customize codec used in the endpoint group. 11 | 12 | JSON codec example: 13 | 14 | ```ts {6-9} 15 | import { h, MetaType, peer, z } from "metapoint"; 16 | import { stringToUint8Array, uint8ArrayToString } from "binconv"; 17 | import destr from "destr"; 18 | 19 | const group = h({ 20 | codec: { 21 | encoder: (data) => stringToUint8Array(JSON.stringify(data)), 22 | decoder: (data) => destr(uint8ArrayToString(data)), 23 | } 24 | }); 25 | 26 | const endpoint = { 27 | helloworld: group.handler({ 28 | func: async ({ data, send, done, context }) => { 29 | await send("hello " + context.world + "!"); 30 | await send("hi " + data); 31 | await done(); 32 | }, 33 | input: z.string(), 34 | output: z.string(), 35 | }), 36 | }; 37 | 38 | const node = await peer({ endpoint }); 39 | 40 | export type Meta = MetaType; 41 | console.log("MetaPoint addr: ", node.meta().addrs); 42 | // /ipv4/127.0.0.1/xxxxxx (it's your server's connect addr) 43 | ``` 44 | 45 | ::: warning Attention 46 | 47 | You need to change the codec of both the server and the client! Otherwise the 48 | data will not be recognized correctly. 49 | 50 | ::: 51 | -------------------------------------------------------------------------------- /docs/src/advance/customLibp2pNode.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Custom Libp2p Node 3 | index: true 4 | order: 9 5 | icon: relation 6 | category: 7 | - Guide 8 | --- 9 | 10 | You can customize the libp2p node when initializing the metapoint peer 11 | 12 | ```ts {2} 13 | import { peer } from "metapoint"; 14 | const node = await peer({ libp2p: customNode }); 15 | ``` 16 | -------------------------------------------------------------------------------- /docs/src/advance/errorCatch.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Error Catching 3 | index: true 4 | order: 5 5 | icon: safe 6 | category: 7 | - Guide 8 | --- 9 | 10 | You can throw an Error directly in the endpoint, and the Error will be passed 11 | directly to the client and then thrown. 12 | 13 | It's like the client is calling a local function. 14 | 15 | In the following example, the server finds that the request has not been 16 | verified and throws an Unauthorized exception, and the client receives it. 17 | 18 | **server** 19 | 20 | ```ts {3-10} 21 | import { h, MetaType, peer, z } from "metapoint"; 22 | 23 | const group = h({ context: { auth: "xxxxxxxxxxxx" }, middleware: async ({ context, next }) => { 24 | if (context.auth === "xxxxxxxxxxxx") { 25 | await next() 26 | } else { 27 | throw new Error("Unauthorized") 28 | } 29 | } 30 | }); 31 | 32 | const endpoint = { 33 | helloworld: group.handler({ 34 | func: async ({ data, send, done }) => { 35 | await send(data); 36 | await done(); 37 | }, 38 | input: z.string(), 39 | output: z.string(), 40 | }), 41 | }; 42 | 43 | const node = await peer({ endpoint }); 44 | 45 | export type Meta = MetaType; 46 | console.log("MetaPoint addr: ", node.meta().addrs); 47 | // /ipv4/127.0.0.1/xxxxxx (it's your server's connect addr) 48 | ``` 49 | 50 | **client** 51 | 52 | ```ts {6-10} 53 | import { peer } from "metapoint"; 54 | import type { Meta } from "./server"; 55 | const node = await peer(); 56 | const channel = await node.connect("your server addr"); 57 | const helloworld = await channel("helloworld"); 58 | try { 59 | console.log(await helloworld("sovlookup")); 60 | } catch (e) { 61 | console.log(e.msg) // Unauthorized 62 | } 63 | ``` 64 | -------------------------------------------------------------------------------- /docs/src/advance/middleware.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Middleware 3 | index: true 4 | order: 4 5 | icon: sort 6 | category: 7 | - Guide 8 | --- 9 | 10 | You can define middleware in endpoint group. 11 | 12 | In Middleware, you can add logic before and after handler execution, or even 13 | stop handler running. 14 | 15 | ```ts {3-13} 16 | import { h, MetaType, peer, z } from "metapoint"; 17 | 18 | const group = h({ middleware: async ({ next, data }) => { 19 | // modify data 20 | newData = data + "addition strings" 21 | if (data === undefined) { 22 | throw new Error("Something went wrong!"); 23 | } 24 | // overwrite the original data 25 | await next({ data: newData }); 26 | console.log("Run successful!") 27 | } 28 | }); 29 | 30 | const endpoint = { 31 | helloworld: group.handler({ 32 | func: async ({ data, send, done }) => { 33 | await send("hello world!"); 34 | await send("hi " + data); 35 | await done(); 36 | }, 37 | input: z.string(), 38 | output: z.string(), 39 | }), 40 | }; 41 | 42 | const node = await peer({ endpoint }); 43 | 44 | export type Meta = MetaType; 45 | console.log("MetaPoint addr: ", node.meta().addrs); 46 | // /ipv4/127.0.0.1/xxxxxx (it's your server's connect addr) 47 | ``` 48 | -------------------------------------------------------------------------------- /docs/src/advance/plugin.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Plugin 3 | index: true 4 | order: 7 5 | icon: plugin 6 | category: 7 | - Guide 8 | --- 9 | 10 | todo 11 | -------------------------------------------------------------------------------- /docs/src/advance/service.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Service 3 | index: true 4 | order: 6 5 | icon: app 6 | category: 7 | - Guide 8 | --- 9 | 10 | For stateful services, you can use service to define endpoints: 11 | 12 | For example, a simple access counting endpoint: 13 | 14 | ```ts {6-17} 15 | import { h, MetaType, peer, z } from "metapoint"; 16 | 17 | const group = h(); 18 | 19 | const endpoint = { 20 | accessCount: group.service({ 21 | func: () => { 22 | let count = 0; 23 | return async ({ send, done }) => { 24 | count += 1; 25 | await send(count) 26 | await done(); 27 | } 28 | }, 29 | input: z.void(), 30 | output: z.number(), 31 | }) 32 | }; 33 | 34 | const node = await peer({ endpoint }); 35 | 36 | export type Meta = MetaType; 37 | console.log("MetaPoint addr: ", node.meta().addrs); 38 | // /ipv4/127.0.0.1/xxxxxx (it's your server's connect addr) 39 | ``` 40 | 41 | `func` in service endpoint is actually a handler factory. 42 | -------------------------------------------------------------------------------- /docs/src/guide/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Guide 3 | index: true 4 | order: 1 5 | icon: guide 6 | category: 7 | - Guide 8 | --- 9 | 10 | MetaPoint based on [Libp2p](https://libp2p.io/) 11 | 12 | In the MetaPoint world, a node is both a server and a client. 13 | 14 | In this guide, we will learn how to use MetaPoint from two starting points: 15 | 16 | - [Server](./server.md) 17 | 18 | - [Client](./client.md) 19 | 20 | You can also get started by learning these real-world examples: 21 | 22 | 1. [Basic example](https://github.com/SOVLOOKUP/metapoint/tree/master/examples/example-basic) 23 | 2. [Vue example](https://github.com/SOVLOOKUP/metapoint/tree/master/examples/example-vue) 24 | 3. [React example](https://github.com/SOVLOOKUP/metapoint/tree/master/examples/example-react) 25 | 4. [Svelte example](https://github.com/SOVLOOKUP/metapoint/tree/master/examples/example-svelte) 26 | 5. [Vue example with custom server addr](https://github.com/SOVLOOKUP/metapoint/tree/master/examples/example-svelte) 27 | -------------------------------------------------------------------------------- /docs/src/guide/client.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Client 3 | icon: tab 4 | index: true 5 | order: 3 6 | next: ../advance/README.md 7 | category: 8 | - Guide 9 | tag: 10 | - client 11 | --- 12 | 13 | ::: tip Tips 14 | 15 | We'll walk through the code line by line, so you can focus on only the 16 | highlighted portion. 17 | 18 | ::: 19 | 20 | First, you need to introduce metapoint and server's meta. 21 | 22 | ```ts {1-2} 23 | import { peer } from "metapoint"; 24 | import type { Meta } from "./server"; 25 | const node = await peer(); 26 | const channel = await node.connect("your server addr"); 27 | const helloworld = await channel("helloworld"); 28 | console.log(await helloworld("sovlookup")); // ["hi sovlookup", "hello world!"] 29 | ``` 30 | 31 | ::: info What's meta? 32 | 33 | - Meta is usually exported on the server side as a type definition. 34 | 35 | - Meta is a typescript type definition that contains all callable functions on 36 | the server side and their signatures. 37 | 38 | - Meta enables you to write client-side code with type hints and autocompletion. 39 | 40 | ::: 41 | 42 | Then, start a metapoint local node(client) and connect to the remote 43 | node(server). 44 | 45 | ```ts {3-4} 46 | import { peer } from "metapoint"; 47 | import type { Meta } from "./server"; 48 | const node = await peer(); 49 | const channel = await node.connect("your server addr"); 50 | const helloworld = await channel("helloworld"); 51 | console.log(await helloworld("sovlookup")); // ["hi sovlookup", "hello world!"] 52 | ``` 53 | 54 | Finally, select the handler that runs on the remote and use it like calling a 55 | local function! 56 | 57 | ```ts {5-6} 58 | import { peer } from "metapoint"; 59 | import type { Meta } from "./server"; 60 | const node = await peer(); 61 | const channel = await node.connect("your server addr"); 62 | const helloworld = await channel("helloworld"); 63 | console.log(await helloworld("sovlookup")); // ["hi sovlookup", "hello world!"] 64 | ``` 65 | 66 | ::: info Use MetaPoint with Web Frameworks 67 | 68 | 1. [Vue example](https://github.com/SOVLOOKUP/metapoint/tree/master/examples/example-vue) 69 | 2. [React example](https://github.com/SOVLOOKUP/metapoint/tree/master/examples/example-react) 70 | 3. [Svelte example](https://github.com/SOVLOOKUP/metapoint/tree/master/examples/example-svelte) 71 | 4. [Vue example with custom server addr](https://github.com/SOVLOOKUP/metapoint/tree/master/examples/example-svelte) 72 | 73 | ::: 74 | -------------------------------------------------------------------------------- /docs/src/guide/server.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Server 3 | icon: software 4 | index: true 5 | order: 2 6 | next: ./client.md 7 | category: 8 | - Guide 9 | tag: 10 | - server 11 | --- 12 | 13 | ::: tip Tips 14 | 15 | We'll walk through the code line by line, so you can focus on only the 16 | highlighted portion 17 | 18 | ::: 19 | 20 | First, you need to introduce metapoint and define an endpoint group 21 | 22 | ::: details Advanced about endpoint group 23 | 24 | Endpoints within the same endpoint group have shared 25 | [context](../advance/channel.md) and 26 | [middleware](../advance/middleware.md) 27 | 28 | ::: 29 | 30 | ```ts {1-3} 31 | import { h, MetaType, peer, z } from "metapoint"; 32 | 33 | const group = h(); 34 | 35 | const endpoint = { 36 | helloworld: group.handler({ 37 | func: async ({ data, send, done }) => { 38 | await send("hello world!"); 39 | await send("hi " + data); 40 | await done(); 41 | }, 42 | input: z.string(), 43 | output: z.string(), 44 | }), 45 | }; 46 | 47 | const node = await peer({ endpoint }); 48 | 49 | export type Meta = MetaType; 50 | console.log("MetaPoint addr: ", node.meta().addrs); 51 | // /ipv4/127.0.0.1/xxxxxx (it's your server's connect addr) 52 | ``` 53 | 54 | Then, you write the endpoint function `helloworld` as a handler and define 55 | `helloworld`'s input and output types 56 | 57 | `helloworld` handler receives your name, response a `hello world!` and then say 58 | hi to you 59 | 60 | ```ts {6-14} 61 | import { h, MetaType, peer, z } from "metapoint"; 62 | 63 | const group = h(); 64 | 65 | const endpoint = { 66 | helloworld: group.handler({ 67 | func: async ({ data, send, done }) => { 68 | await send("hello world!"); 69 | await send(data); 70 | await done(); 71 | }, 72 | input: z.string(), 73 | output: z.string(), 74 | }), 75 | }; 76 | const node = await peer({ endpoint }); 77 | 78 | export type Meta = MetaType; 79 | console.log("MetaPoint addr: ", node.meta().addrs); 80 | // /ipv4/127.0.0.1/xxxxxx (it's your server's connect addr) 81 | ``` 82 | 83 | ::: info Why done()? 84 | 85 | Function `send` will not end the calling procedure of the handler, you need to 86 | call the `done` function manually, because there may be multiple replies in one 87 | handler. 88 | 89 | ::: 90 | 91 | You can define more endpoints in the endpoint object: 92 | 93 | ```ts {11-17} 94 | const endpoint = { 95 | helloworld: group.handler({ 96 | func: async ({ data, send, done }) => { 97 | await send("hello world!"); 98 | await send(data); 99 | await done(); 100 | }, 101 | input: z.string(), 102 | output: z.string(), 103 | }), 104 | your_own_endpoint: group.handler({ 105 | func: async ({ data, send, done }) => { 106 | ... 107 | }, 108 | input: ..., 109 | output: ..., 110 | }), 111 | }; 112 | ``` 113 | 114 | Finally, pass the endpoints to the `peer` function to start a metapoint node, 115 | metapoint will automatically serve your endpoints 116 | 117 | ```ts {16} 118 | import { h, MetaType, peer, z } from "metapoint"; 119 | 120 | const group = h(); 121 | 122 | const endpoint = { 123 | helloworld: group.handler({ 124 | func: async ({ data, send, done }) => { 125 | await send("hello world!"); 126 | await send(data); 127 | await done(); 128 | }, 129 | input: z.string(), 130 | output: z.string(), 131 | }), 132 | }; 133 | const node = await peer({ endpoint }); 134 | 135 | export type Meta = MetaType; 136 | console.log("MetaPoint addr: ", node.meta().addrs); 137 | // /ipv4/127.0.0.1/xxxxxx (it's your server's connect addr) 138 | ``` 139 | 140 | Here we exported the meta and printed our address for client connection 141 | 142 | ```ts {18-19} 143 | import { h, MetaType, peer, z } from "metapoint"; 144 | 145 | const group = h(); 146 | 147 | const endpoint = { 148 | helloworld: group.handler({ 149 | func: async ({ data, send, done }) => { 150 | await send("hello world!"); 151 | await send(data); 152 | await done(); 153 | }, 154 | input: z.string(), 155 | output: z.string(), 156 | }), 157 | }; 158 | const node = await peer({ endpoint }); 159 | 160 | export type Meta = MetaType; 161 | console.log("MetaPoint addr: ", node.meta().addrs); 162 | // /ipv4/127.0.0.1/xxxxxx (it's your server's connect addr) 163 | ``` 164 | 165 | ::: info What's meta? 166 | 167 | meta is just the type definition of endpoints, used for type hinting and 168 | autocompletion on the client 169 | 170 | you can also export the Meta like this: 171 | 172 | ```ts {12} 173 | const endpoint = { 174 | helloworld: group.handler({ 175 | func: async ({ data, send, done }) => { 176 | await send("hello world!"); 177 | await send(data); 178 | await done(); 179 | }, 180 | input: z.string(), 181 | output: z.string(), 182 | }), 183 | }; 184 | export type Meta = typeof endpoint; 185 | ``` 186 | 187 | ::: 188 | -------------------------------------------------------------------------------- /docs/src/zh/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MetaPointTech/metapoint/09b1f5976a1345b9c9db945a0ba8f771e3e35b4e/docs/src/zh/.gitkeep -------------------------------------------------------------------------------- /docs/src/zh/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | home: true 3 | icon: home 4 | title: MetaPoint 5 | heroImage: /logo.png 6 | heroText: MetaPoint 7 | tagline: ⚡简洁的低代码 RPC 框架, 使用 TypeScript 极速编写全栈项目。 8 | actions: 9 | - text: 快速入门 💡 10 | link: /guide/ 11 | type: primary 12 | 13 | - text: ⭐ 点亮星星 14 | link: https://github.com/SOVLOOKUP/metapoint/stargazers 15 | 16 | copyright: Copyright © 2023 SOVLOOKUP 17 | footer: Apache-2.0 Licensed 18 | --- 19 | -------------------------------------------------------------------------------- /examples/example-basic/index.ts: -------------------------------------------------------------------------------- 1 | import { peer } from "metapoint"; 2 | import type { Meta } from "./server"; 3 | import addr from "./server"; 4 | const node = await peer(); 5 | const channel = await node.connect(addr); 6 | const add = await channel("add"); 7 | console.log(await add(1)); // [2] 8 | -------------------------------------------------------------------------------- /examples/example-basic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "1.0.0", 4 | "description": "", 5 | "type": "module", 6 | "dependencies": { 7 | "metapoint": "*" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/example-basic/server.ts: -------------------------------------------------------------------------------- 1 | import { h, MetaType, peer, z } from "metapoint"; 2 | 3 | const group = h({ context: { addnum: 1 } }); 4 | 5 | const endpoint = { 6 | add: group.handler({ 7 | func: async ({ data, send, done, context }) => { 8 | await send(data + context.addnum); 9 | await done(); 10 | }, 11 | input: z.number(), 12 | output: z.number(), 13 | }), 14 | }; 15 | 16 | const node = await peer({ endpoint }); 17 | export type Meta = MetaType; 18 | export default node.meta().addrs; 19 | -------------------------------------------------------------------------------- /examples/example-basic/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "target": "ESNext", 5 | "module": "ESNext", 6 | }, 7 | "exclude": ["node_modules"] 8 | } -------------------------------------------------------------------------------- /examples/example-react/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /examples/example-react/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/example-react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-react", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite" 8 | }, 9 | "dependencies": { 10 | "metapoint": "workspace:*", 11 | "react": "^18.2.0", 12 | "react-dom": "^18.2.0" 13 | }, 14 | "devDependencies": { 15 | "@types/react": "^18.2.37", 16 | "@types/react-dom": "^18.2.15", 17 | "@vitejs/plugin-react-swc": "^3.4.1", 18 | "typescript": "^5.2.2", 19 | "vite": "^4.5.0" 20 | } 21 | } -------------------------------------------------------------------------------- /examples/example-react/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/example-react/src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .logo { 9 | height: 6em; 10 | padding: 1.5em; 11 | will-change: filter; 12 | transition: filter 300ms; 13 | } 14 | .logo:hover { 15 | filter: drop-shadow(0 0 2em #646cffaa); 16 | } 17 | .logo.react:hover { 18 | filter: drop-shadow(0 0 2em #61dafbaa); 19 | } 20 | 21 | @keyframes logo-spin { 22 | from { 23 | transform: rotate(0deg); 24 | } 25 | to { 26 | transform: rotate(360deg); 27 | } 28 | } 29 | 30 | @media (prefers-reduced-motion: no-preference) { 31 | a:nth-of-type(2) .logo { 32 | animation: logo-spin infinite 20s linear; 33 | } 34 | } 35 | 36 | .card { 37 | padding: 2em; 38 | } 39 | 40 | .read-the-docs { 41 | color: #888; 42 | } 43 | -------------------------------------------------------------------------------- /examples/example-react/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import reactLogo from './assets/react.svg' 3 | import './App.css' 4 | import type { Meta } from '../../example-basic/server' 5 | import { peer } from 'metapoint' 6 | 7 | const node = await peer() 8 | const channel = await node.connect((window as any)['addr'].reverse()) 9 | const plus = await channel("add") 10 | 11 | function App() { 12 | const [count, setCount] = useState(0) 13 | 14 | return ( 15 |
16 | 24 |

MetaPoint + React

25 |

26 | { (window as any).addr.join("\n")} 27 |

28 |
29 | 35 |
36 |
37 | ) 38 | } 39 | 40 | export default App 41 | -------------------------------------------------------------------------------- /examples/example-react/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/example-react/src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-weight: 400; 5 | 6 | color-scheme: light dark; 7 | color: rgba(255, 255, 255, 0.87); 8 | background-color: #242424; 9 | 10 | font-synthesis: none; 11 | text-rendering: optimizeLegibility; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | -webkit-text-size-adjust: 100%; 15 | } 16 | 17 | a { 18 | font-weight: 500; 19 | color: #646cff; 20 | text-decoration: inherit; 21 | } 22 | a:hover { 23 | color: #535bf2; 24 | } 25 | 26 | body { 27 | margin: 0; 28 | display: flex; 29 | place-items: center; 30 | min-width: 320px; 31 | min-height: 100vh; 32 | } 33 | 34 | h1 { 35 | font-size: 3.2em; 36 | line-height: 1.1; 37 | } 38 | 39 | button { 40 | border-radius: 8px; 41 | border: 1px solid transparent; 42 | padding: 0.6em 1.2em; 43 | font-size: 1em; 44 | font-weight: 500; 45 | font-family: inherit; 46 | background-color: #1a1a1a; 47 | cursor: pointer; 48 | transition: border-color 0.25s; 49 | } 50 | button:hover { 51 | border-color: #646cff; 52 | } 53 | button:focus, 54 | button:focus-visible { 55 | outline: 4px auto -webkit-focus-ring-color; 56 | } 57 | 58 | @media (prefers-color-scheme: light) { 59 | :root { 60 | color: #213547; 61 | background-color: #ffffff; 62 | } 63 | a:hover { 64 | color: #747bff; 65 | } 66 | button { 67 | background-color: #f9f9f9; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /examples/example-react/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import App from './App' 4 | import './index.css' 5 | 6 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( 7 | 8 | 9 | , 10 | ) 11 | -------------------------------------------------------------------------------- /examples/example-react/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/example-react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"], 20 | "references": [{ "path": "./tsconfig.node.json" }] 21 | } 22 | -------------------------------------------------------------------------------- /examples/example-react/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /examples/example-react/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react-swc' 3 | // @ts-ignore 4 | import addr from "../example-basic/server"; 5 | 6 | // https://vitejs.dev/config/ 7 | export default defineConfig({ 8 | define: { 9 | addr, 10 | }, 11 | plugins: [react()], 12 | }) 13 | -------------------------------------------------------------------------------- /examples/example-svelte/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /examples/example-svelte/README.md: -------------------------------------------------------------------------------- 1 | # Svelte + TS + Vite 2 | 3 | This template should help get you started developing with Svelte and TypeScript in Vite. 4 | 5 | ## Recommended IDE Setup 6 | 7 | [VS Code](https://code.visualstudio.com/) + [Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode). 8 | 9 | ## Need an official Svelte framework? 10 | 11 | Check out [SvelteKit](https://github.com/sveltejs/kit#readme), which is also powered by Vite. Deploy anywhere with its serverless-first approach and adapt to various platforms, with out of the box support for TypeScript, SCSS, and Less, and easily-added support for mdsvex, GraphQL, PostCSS, Tailwind CSS, and more. 12 | 13 | ## Technical considerations 14 | 15 | **Why use this over SvelteKit?** 16 | 17 | - It brings its own routing solution which might not be preferable for some users. 18 | - It is first and foremost a framework that just happens to use Vite under the hood, not a Vite app. 19 | 20 | This template contains as little as possible to get started with Vite + TypeScript + Svelte, while taking into account the developer experience with regards to HMR and intellisense. It demonstrates capabilities on par with the other `create-vite` templates and is a good starting point for beginners dipping their toes into a Vite + Svelte project. 21 | 22 | Should you later need the extended capabilities and extensibility provided by SvelteKit, the template has been structured similarly to SvelteKit so that it is easy to migrate. 23 | 24 | **Why `global.d.ts` instead of `compilerOptions.types` inside `jsconfig.json` or `tsconfig.json`?** 25 | 26 | Setting `compilerOptions.types` shuts out all other types not explicitly listed in the configuration. Using triple-slash references keeps the default TypeScript setting of accepting type information from the entire workspace, while also adding `svelte` and `vite/client` type information. 27 | 28 | **Why include `.vscode/extensions.json`?** 29 | 30 | Other templates indirectly recommend extensions via the README, but this file allows VS Code to prompt the user to install the recommended extension upon opening the project. 31 | 32 | **Why enable `allowJs` in the TS template?** 33 | 34 | While `allowJs: false` would indeed prevent the use of `.js` files in the project, it does not prevent the use of JavaScript syntax in `.svelte` files. In addition, it would force `checkJs: false`, bringing the worst of both worlds: not being able to guarantee the entire codebase is TypeScript, and also having worse typechecking for the existing JavaScript. In addition, there are valid use cases in which a mixed codebase may be relevant. 35 | 36 | **Why is HMR not preserving my local component state?** 37 | 38 | HMR state preservation comes with a number of gotchas! It has been disabled by default in both `svelte-hmr` and `@sveltejs/vite-plugin-svelte` due to its often surprising behavior. You can read the details [here](https://github.com/rixo/svelte-hmr#svelte-hmr). 39 | 40 | If you have state that's important to retain within a component, consider creating an external store which would not be replaced by HMR. 41 | 42 | ```ts 43 | // store.ts 44 | // An extremely simple external store 45 | import { writable } from 'svelte/store' 46 | export default writable(0) 47 | ``` 48 | -------------------------------------------------------------------------------- /examples/example-svelte/__unconfig_vite.config.ts: -------------------------------------------------------------------------------- 1 | 2 | let __unconfig_data; 3 | let __unconfig_stub = function (data = {}) { __unconfig_data = data }; 4 | __unconfig_stub.default = (data = {}) => { __unconfig_data = data }; 5 | import { defineConfig } from "vite"; 6 | import { svelte } from "@sveltejs/vite-plugin-svelte"; 7 | // @ts-ignore 8 | import addr from "../example/server"; 9 | 10 | // https://vitejs.dev/config/ 11 | const __unconfig_default = defineConfig({ 12 | define: { 13 | addr, 14 | }, 15 | plugins: [svelte()], 16 | }); 17 | 18 | if (typeof __unconfig_default === "function") __unconfig_default(...[{"command":"serve","mode":"development"}]);export default __unconfig_data; -------------------------------------------------------------------------------- /examples/example-svelte/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + Svelte + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/example-svelte/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-svelte", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite" 8 | }, 9 | "devDependencies": { 10 | "@sveltejs/vite-plugin-svelte": "^2.4.6", 11 | "@tsconfig/svelte": "^5.0.2", 12 | "metapoint": "workspace:*", 13 | "svelte": "^4.2.2", 14 | "svelte-check": "^3.5.2", 15 | "tslib": "^2.6.2", 16 | "typescript": "^5.2.2", 17 | "vite": "^4.5.0" 18 | } 19 | } -------------------------------------------------------------------------------- /examples/example-svelte/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/example-svelte/src/App.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 |
7 | 23 |

MetaPoint + Svelte

24 | 25 | {window['addr'].join('\n')} 26 |
27 | 28 |
29 |
30 | 31 | 45 | -------------------------------------------------------------------------------- /examples/example-svelte/src/app.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-weight: 400; 5 | 6 | color-scheme: light dark; 7 | color: rgba(255, 255, 255, 0.87); 8 | background-color: #242424; 9 | 10 | font-synthesis: none; 11 | text-rendering: optimizeLegibility; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | -webkit-text-size-adjust: 100%; 15 | } 16 | 17 | a { 18 | font-weight: 500; 19 | color: #646cff; 20 | text-decoration: inherit; 21 | } 22 | a:hover { 23 | color: #535bf2; 24 | } 25 | 26 | body { 27 | margin: 0; 28 | display: flex; 29 | place-items: center; 30 | min-width: 320px; 31 | min-height: 100vh; 32 | } 33 | 34 | h1 { 35 | font-size: 3.2em; 36 | line-height: 1.1; 37 | } 38 | 39 | .card { 40 | padding: 2em; 41 | } 42 | 43 | #app { 44 | max-width: 1280px; 45 | margin: 0 auto; 46 | padding: 2rem; 47 | text-align: center; 48 | } 49 | 50 | button { 51 | border-radius: 8px; 52 | border: 1px solid transparent; 53 | padding: 0.6em 1.2em; 54 | font-size: 1em; 55 | font-weight: 500; 56 | font-family: inherit; 57 | background-color: #1a1a1a; 58 | cursor: pointer; 59 | transition: border-color 0.25s; 60 | } 61 | button:hover { 62 | border-color: #646cff; 63 | } 64 | button:focus, 65 | button:focus-visible { 66 | outline: 4px auto -webkit-focus-ring-color; 67 | } 68 | 69 | @media (prefers-color-scheme: light) { 70 | :root { 71 | color: #213547; 72 | background-color: #ffffff; 73 | } 74 | a:hover { 75 | color: #747bff; 76 | } 77 | button { 78 | background-color: #f9f9f9; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /examples/example-svelte/src/assets/svelte.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/example-svelte/src/lib/Counter.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | {#await main()} 15 |

connecting...

16 | {:then plus} 17 | 20 | {:catch error} 21 |

{error.message}

22 | {/await} 23 | -------------------------------------------------------------------------------- /examples/example-svelte/src/main.ts: -------------------------------------------------------------------------------- 1 | import "./app.css"; 2 | import App from "./App.svelte"; 3 | 4 | const app = new App({ 5 | target: document.getElementById("app"), 6 | }); 7 | 8 | export default app; 9 | -------------------------------------------------------------------------------- /examples/example-svelte/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /examples/example-svelte/svelte.config.js: -------------------------------------------------------------------------------- 1 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' 2 | 3 | export default { 4 | // Consult https://svelte.dev/docs#compile-time-svelte-preprocess 5 | // for more information about preprocessors 6 | preprocess: vitePreprocess(), 7 | } 8 | -------------------------------------------------------------------------------- /examples/example-svelte/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/svelte/tsconfig.json", 3 | "compilerOptions": { 4 | "target": "ESNext", 5 | "useDefineForClassFields": true, 6 | "module": "ESNext", 7 | "resolveJsonModule": true, 8 | /** 9 | * Typecheck JS in `.svelte` and `.js` files by default. 10 | * Disable checkJs if you'd like to use dynamic types in JS. 11 | * Note that setting allowJs false does not prevent the use 12 | * of JS in `.svelte` files. 13 | */ 14 | "allowJs": true, 15 | "checkJs": true, 16 | "isolatedModules": true 17 | }, 18 | "include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte"], 19 | "references": [{ "path": "./tsconfig.node.json" }] 20 | } 21 | -------------------------------------------------------------------------------- /examples/example-svelte/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node" 6 | }, 7 | "include": ["vite.config.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /examples/example-svelte/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import { svelte } from "@sveltejs/vite-plugin-svelte"; 3 | // @ts-ignore 4 | import addr from "../example-basic/server"; 5 | 6 | // https://vitejs.dev/config/ 7 | export default defineConfig({ 8 | define: { 9 | addr, 10 | }, 11 | plugins: [svelte()], 12 | }); 13 | -------------------------------------------------------------------------------- /examples/example-vue-customAddr/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /examples/example-vue-customAddr/README.md: -------------------------------------------------------------------------------- 1 | # Vue 3 + TypeScript + Vite 2 | 3 | This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 ` 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/example-vue-customAddr/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "metapoint-demo", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "dist": "vue-tsc && vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "metapoint": "workspace:*", 13 | "vue": "^3.3.8" 14 | }, 15 | "devDependencies": { 16 | "@vitejs/plugin-vue": "^4.4.0", 17 | "typescript": "^5.2.2", 18 | "vite": "^4.5.0", 19 | "vue-tsc": "^1.8.22" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/example-vue-customAddr/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/example-vue-customAddr/server.ts: -------------------------------------------------------------------------------- 1 | import { h, MetaType, peer, z } from "metapoint"; 2 | 3 | const g = h(); 4 | 5 | const endpoint = { 6 | numberAdd: g.handler({ 7 | input: z.number(), 8 | output: z.number(), 9 | func: async ({ data, send, done }) => { 10 | const result = data + 1; 11 | await send(result); 12 | await done(); 13 | }, 14 | }), 15 | numberAdd1: g.handler({ 16 | input: z.number(), 17 | output: z.number(), 18 | func: async ({ data, send, done }) => { 19 | const result = data + 1; 20 | await send(result); 21 | await done(); 22 | }, 23 | }), 24 | numberAdd2: g.handler({ 25 | input: z.number(), 26 | output: z.number(), 27 | func: async ({ data, send, done }) => { 28 | const result = data + 1; 29 | await send(result); 30 | await done(); 31 | }, 32 | }), 33 | numberAdd3: g.handler({ 34 | input: z.number(), 35 | output: z.number(), 36 | func: async ({ data, send, done }) => { 37 | const result = data + 1; 38 | await send(result); 39 | await done(); 40 | }, 41 | }), 42 | }; 43 | 44 | const server = await peer({ endpoint }); 45 | 46 | export type meta = MetaType; 47 | const addrs = server.meta().addrs; 48 | 49 | console.log(addrs); 50 | -------------------------------------------------------------------------------- /examples/example-vue-customAddr/src/App.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 34 | 35 | -------------------------------------------------------------------------------- /examples/example-vue-customAddr/src/assets/vue.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/example-vue-customAddr/src/client.ts: -------------------------------------------------------------------------------- 1 | import { peer } from "metapoint"; 2 | import type { meta } from "../server"; 3 | 4 | const client = await peer(); 5 | 6 | export const newChan = async (...addrs: string[]) => 7 | await client.connect(addrs); 8 | -------------------------------------------------------------------------------- /examples/example-vue-customAddr/src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 33 | 34 | 39 | -------------------------------------------------------------------------------- /examples/example-vue-customAddr/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from "vue"; 2 | import "./style.css"; 3 | import App from "./App.vue"; 4 | 5 | createApp(App).mount("#app"); 6 | -------------------------------------------------------------------------------- /examples/example-vue-customAddr/src/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-weight: 400; 5 | 6 | color-scheme: light dark; 7 | color: rgba(255, 255, 255, 0.87); 8 | background-color: #242424; 9 | 10 | font-synthesis: none; 11 | text-rendering: optimizeLegibility; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | -webkit-text-size-adjust: 100%; 15 | } 16 | 17 | a { 18 | font-weight: 500; 19 | color: #646cff; 20 | text-decoration: inherit; 21 | } 22 | a:hover { 23 | color: #535bf2; 24 | } 25 | 26 | body { 27 | margin: 0; 28 | display: flex; 29 | place-items: center; 30 | min-width: 320px; 31 | min-height: 100vh; 32 | } 33 | 34 | h1 { 35 | font-size: 3.2em; 36 | line-height: 1.1; 37 | } 38 | 39 | button { 40 | border-radius: 8px; 41 | border: 1px solid transparent; 42 | padding: 0.6em 1.2em; 43 | font-size: 1em; 44 | font-weight: 500; 45 | font-family: inherit; 46 | background-color: #1a1a1a; 47 | cursor: pointer; 48 | transition: border-color 0.25s; 49 | } 50 | button:hover { 51 | border-color: #646cff; 52 | } 53 | button:focus, 54 | button:focus-visible { 55 | outline: 4px auto -webkit-focus-ring-color; 56 | } 57 | 58 | .card { 59 | padding: 2em; 60 | } 61 | 62 | #app { 63 | max-width: 1280px; 64 | margin: 0 auto; 65 | padding: 2rem; 66 | text-align: center; 67 | } 68 | 69 | @media (prefers-color-scheme: light) { 70 | :root { 71 | color: #213547; 72 | background-color: #ffffff; 73 | } 74 | a:hover { 75 | color: #747bff; 76 | } 77 | button { 78 | background-color: #f9f9f9; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /examples/example-vue-customAddr/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/example-vue-customAddr/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "moduleResolution": "Node", 7 | "strict": true, 8 | "jsx": "preserve", 9 | "resolveJsonModule": true, 10 | "isolatedModules": true, 11 | "esModuleInterop": true, 12 | "lib": ["ESNext", "DOM"], 13 | "skipLibCheck": true, 14 | "noEmit": true 15 | }, 16 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], 17 | "references": [{ "path": "./tsconfig.node.json" }] 18 | } 19 | -------------------------------------------------------------------------------- /examples/example-vue-customAddr/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /examples/example-vue-customAddr/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import vue from "@vitejs/plugin-vue"; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [vue()], 7 | build: { 8 | target: "esnext", 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /examples/example-vue/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /examples/example-vue/README.md: -------------------------------------------------------------------------------- 1 | # Vue 3 + TypeScript + Vite + MetaPoint 2 | -------------------------------------------------------------------------------- /examples/example-vue/__unconfig_vite.config.ts: -------------------------------------------------------------------------------- 1 | 2 | let __unconfig_data; 3 | let __unconfig_stub = function (data = {}) { __unconfig_data = data }; 4 | __unconfig_stub.default = (data = {}) => { __unconfig_data = data }; 5 | import { defineConfig } from 'vite' 6 | import vue from '@vitejs/plugin-vue' 7 | // @ts-ignore 8 | import addr from "../example/server"; 9 | 10 | // https://vitejs.dev/config/ 11 | const __unconfig_default = defineConfig({ 12 | define: { 13 | addr, 14 | }, 15 | plugins: [vue()], 16 | }) 17 | 18 | if (typeof __unconfig_default === "function") __unconfig_default(...[{"command":"serve","mode":"development"}]);export default __unconfig_data; -------------------------------------------------------------------------------- /examples/example-vue/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + Vue + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/example-vue/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-vue", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite" 8 | }, 9 | "dependencies": { 10 | "metapoint": "workspace:*", 11 | "vue": "^3.3.8" 12 | }, 13 | "devDependencies": { 14 | "@vitejs/plugin-vue": "^4.4.0", 15 | "typescript": "^5.2.2", 16 | "vite": "^4.5.0", 17 | "vue-tsc": "^1.8.22" 18 | } 19 | } -------------------------------------------------------------------------------- /examples/example-vue/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/example-vue/src/App.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 18 | 19 | 35 | -------------------------------------------------------------------------------- /examples/example-vue/src/assets/vue.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/example-vue/src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 24 | 25 | 30 | -------------------------------------------------------------------------------- /examples/example-vue/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import './style.css' 3 | import App from './App.vue' 4 | 5 | createApp(App).mount('#app') 6 | -------------------------------------------------------------------------------- /examples/example-vue/src/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-weight: 400; 5 | 6 | color-scheme: light dark; 7 | color: rgba(255, 255, 255, 0.87); 8 | background-color: #242424; 9 | 10 | font-synthesis: none; 11 | text-rendering: optimizeLegibility; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | -webkit-text-size-adjust: 100%; 15 | } 16 | 17 | a { 18 | font-weight: 500; 19 | color: #646cff; 20 | text-decoration: inherit; 21 | } 22 | a:hover { 23 | color: #535bf2; 24 | } 25 | 26 | body { 27 | margin: 0; 28 | display: flex; 29 | place-items: center; 30 | min-width: 320px; 31 | min-height: 100vh; 32 | } 33 | 34 | h1 { 35 | font-size: 3.2em; 36 | line-height: 1.1; 37 | } 38 | 39 | button { 40 | border-radius: 8px; 41 | border: 1px solid transparent; 42 | padding: 0.6em 1.2em; 43 | font-size: 1em; 44 | font-weight: 500; 45 | font-family: inherit; 46 | background-color: #1a1a1a; 47 | cursor: pointer; 48 | transition: border-color 0.25s; 49 | } 50 | button:hover { 51 | border-color: #646cff; 52 | } 53 | button:focus, 54 | button:focus-visible { 55 | outline: 4px auto -webkit-focus-ring-color; 56 | } 57 | 58 | .card { 59 | padding: 2em; 60 | } 61 | 62 | #app { 63 | max-width: 1280px; 64 | margin: 0 auto; 65 | padding: 2rem; 66 | text-align: center; 67 | } 68 | 69 | @media (prefers-color-scheme: light) { 70 | :root { 71 | color: #213547; 72 | background-color: #ffffff; 73 | } 74 | a:hover { 75 | color: #747bff; 76 | } 77 | button { 78 | background-color: #f9f9f9; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /examples/example-vue/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/example-vue/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "moduleResolution": "Node", 7 | "strict": true, 8 | "jsx": "preserve", 9 | "resolveJsonModule": true, 10 | "isolatedModules": true, 11 | "esModuleInterop": true, 12 | "lib": ["ESNext", "DOM"], 13 | "skipLibCheck": true, 14 | "noEmit": true 15 | }, 16 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], 17 | "references": [{ "path": "./tsconfig.node.json" }] 18 | } 19 | -------------------------------------------------------------------------------- /examples/example-vue/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /examples/example-vue/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | // @ts-ignore 4 | import addr from "../example-basic/server"; 5 | 6 | // https://vitejs.dev/config/ 7 | export default defineConfig({ 8 | define: { 9 | addr, 10 | }, 11 | plugins: [vue()], 12 | }) 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "tsup": "^7.2.0", 4 | "typescript": "^5.2.2", 5 | "vitest": "^0.34.6" 6 | } 7 | } -------------------------------------------------------------------------------- /packages/libp2p-transport/README.md: -------------------------------------------------------------------------------- 1 | # libp2p-transport 2 | 3 | peer to peer rpc/channel communication based on libp2p 4 | 5 | ## Features 6 | 7 | - ⚡Efficient transmission 8 | - 🤝Bidirectional channel support 9 | - 🎡AsyncIterator style 10 | - 🪐Peer to peer connect 11 | - 🔢Data codec agnostic(json, protobuff, etc.) 12 | - 📡Transport protocol agnostic(tcp/udp, ws, webtransport, etc.) 13 | - 💪Cross-peer error capture 14 | 15 | ## Quickstart 16 | 17 | **first** 18 | 19 | ```typescript 20 | import { client, server } from "libp2p-transport"; 21 | 22 | // server side 23 | const { handle, serve } = server(libp2p); 24 | // client side 25 | const conn = await client(libp2p, addr); 26 | ``` 27 | 28 | **↓↓↓ USAGE ↓↓↓** 29 | 30 | 1. [ ] **One request one response** 31 | 32 | ```typescript 33 | // server side 34 | await handle( 35 | "add", 36 | async ({ data, send, done }) => { 37 | await send(data + 1); 38 | await done(); 39 | }, 40 | ); 41 | // client side 42 | const add = await conn("add"); 43 | await add.send(1); 44 | (await add.next()).value; // 2 45 | // or 46 | await add(1); // [2] 47 | ``` 48 | 49 | 2. [ ] **One request multi response** 50 | 51 | ```typescript 52 | // server side 53 | await handle( 54 | "adding", 55 | async ({ data, send, done }) => { 56 | await send(data + 1); 57 | await send(data + 2); 58 | await send(data + 3); 59 | await done(); 60 | }, 61 | ); 62 | // client side 63 | const adding = await conn("adding"); 64 | await adding.send(1); 65 | for await (const msg of adding) { 66 | console.log(msg); // 2, 3, 4 67 | } 68 | // or 69 | await adding(1); // [2, 3, 4] 70 | ``` 71 | 72 | 3. [ ] **Bidirectional channel(infinite request infinite response)** 73 | 74 | ```typescript 75 | // server side 76 | const service = () => { 77 | const handle = ({ data, send }) => send(data + 1); 78 | return handle; 79 | }; 80 | await serve( 81 | "channelAdd", 82 | service, 83 | ); 84 | // client side 85 | const channelAdd = await conn("channelAdd"); 86 | let num = 0; 87 | await channelAdd.send(num); 88 | for await (const msg of channelAdd) { 89 | msg === num; // true 90 | num++; 91 | await channel.send(num); 92 | } 93 | ``` 94 | 95 | more examples in [test](./test/index.test.ts) 96 | -------------------------------------------------------------------------------- /packages/libp2p-transport/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "libp2p-transport", 3 | "version": "0.4.2", 4 | "description": "peer to peer rpc/channel communication based on libp2p", 5 | "keywords": [ 6 | "libp2p", 7 | "channel", 8 | "rpc" 9 | ], 10 | "author": "sovlookup ", 11 | "license": "MIT", 12 | "homepage": "https://github.com/SOVLOOKUP/metapoint/tree/master/packages/core", 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/SOVLOOKUP/metapoint" 16 | }, 17 | "bugs": { 18 | "url": "https://github.com/SOVLOOKUP/metapoint/issues" 19 | }, 20 | "type": "module", 21 | "main": "dist/index.cjs", 22 | "module": "dist/index.js", 23 | "types": "dist/index.d.ts", 24 | "exports": { 25 | "import": "./dist/index.js", 26 | "types": "./dist/index.d.ts", 27 | "require": "./dist/index.cts" 28 | }, 29 | "files": [ 30 | "dist/*", 31 | "README.md" 32 | ], 33 | "scripts": { 34 | "build": "tsup --cjsInterop", 35 | "test": "vitest run", 36 | "bench": "vitest bench" 37 | }, 38 | "dependencies": { 39 | "@libp2p/interface": "^1.0.1", 40 | "@libp2p/peer-id": "^4.0.1", 41 | "@multiformats/multiaddr": "^12.1.11", 42 | "@xobj/core": "^1.3.2", 43 | "libp2p": "^1.0.8", 44 | "pino": "^8.16.2", 45 | "queueable": "^5.3.2", 46 | "streaming-iterables": "^8.0.1" 47 | }, 48 | "devDependencies": { 49 | "@chainsafe/libp2p-noise": "^14.0.0", 50 | "@libp2p/mplex": "^10.0.5", 51 | "@libp2p/websockets": "^8.0.5", 52 | "binconv": "^0.2.0", 53 | "destr": "^2.0.2", 54 | "fastify": "^4.24.3" 55 | }, 56 | "tsup": { 57 | "entry": [ 58 | "src/index.ts" 59 | ], 60 | "format": [ 61 | "cjs", 62 | "esm" 63 | ], 64 | "clean": true, 65 | "dts": true, 66 | "treeshake": true, 67 | "shims": true 68 | } 69 | } -------------------------------------------------------------------------------- /packages/libp2p-transport/src/client.ts: -------------------------------------------------------------------------------- 1 | import { type AnyIterable, transform, collect, consume } from "streaming-iterables"; 2 | import { control_name, defaultInitOptions } from "./common"; 3 | import type { 4 | Chan, 5 | Codec, 6 | ControlMsg, 7 | InitOptions, 8 | PeerAddr, 9 | TransportChannel, 10 | } from "./types"; 11 | import type { PeerId, Connection, Stream } from "@libp2p/interface"; 12 | import { peerIdFromString } from "@libp2p/peer-id"; 13 | import { isPeerId } from "@libp2p/interface"; 14 | import { Channel } from "queueable"; 15 | import type { Multiaddr } from "@multiformats/multiaddr"; 16 | import { isMultiaddr, multiaddr } from "@multiformats/multiaddr"; 17 | import { Libp2p } from "libp2p"; 18 | import { newChannel } from "./utils"; 19 | import { runtimeError } from "./error"; 20 | import { logger } from "./logger"; 21 | import { defaultCodec } from "./codec"; 22 | 23 | const ccs = new Map>(); 24 | 25 | const fetchStream = ( 26 | stream: Stream, 27 | input: AnyIterable, 28 | codec: Codec, 29 | ) => { 30 | const inputIterator = transform( 31 | Infinity, 32 | (data) => codec.encoder(data), 33 | input, 34 | ); 35 | const outputIterator = transform( 36 | Infinity, 37 | async (data) => await codec.decoder(data.subarray()) as Awaited, 38 | stream.source, 39 | ); 40 | stream.sink(inputIterator); 41 | return outputIterator; 42 | }; 43 | 44 | const handleControlMsg = async (id: string, cc?: Channel) => { 45 | if (cc === undefined) cc = ccs.get(id) as Channel; 46 | const msg = (await cc.next()).value as ControlMsg; 47 | cc.close(); 48 | // maybe has been done already 49 | if (msg) { 50 | try { 51 | switch (msg.type) { 52 | case "error": 53 | const err = new Error(msg.message); 54 | err.name = msg.name; 55 | err.stack = msg.stack; 56 | throw err; 57 | 58 | default: 59 | break; 60 | } 61 | } finally { 62 | ccs.delete(id); 63 | } 64 | } 65 | }; 66 | 67 | const channel = async ( 68 | name: string, 69 | connection: Connection, 70 | options?: InitOptions, 71 | ): Promise< 72 | & ((value: I) => Promise) 73 | & TransportChannel 74 | > => { 75 | const runtimeOptions = { 76 | ...defaultInitOptions, 77 | ...options, 78 | }; 79 | 80 | logger.trace(`new connecting to protocol ${name}`); 81 | let inputChannel: Channel = new Channel(); 82 | let stream: Stream = await connection.newStream(name); 83 | let outputIterator: AsyncIterableIterator = fetchStream( 84 | stream, 85 | inputChannel, 86 | runtimeOptions.codec, 87 | ); 88 | let cc: Channel = new Channel(); 89 | let chan: Chan = newChannel( 90 | inputChannel, 91 | { connection, stream }, 92 | undefined, 93 | runtimeOptions.context, 94 | ); 95 | 96 | const jid = JSON.stringify(chan.id); 97 | // receive first value as id 98 | await chan.send(jid as I); 99 | if (name !== control_name) ccs.set(jid, cc); 100 | logger.trace(`protocol ${name} connected`); 101 | 102 | // Connect iterator end to end 103 | const op = new Proxy({ 104 | ...outputIterator, 105 | ...chan, 106 | }, { 107 | get(target, p) { 108 | if (p === Symbol.asyncIterator) { 109 | return async function* () { 110 | for await (const v of target[Symbol.asyncIterator]()) { 111 | yield v; 112 | } 113 | await target.done(); 114 | await handleControlMsg(jid, cc); 115 | }; 116 | } 117 | if (p === "next") { 118 | return async () => { 119 | const result = await target.next(); 120 | if (result.done === true) { 121 | await target.done(); 122 | await handleControlMsg(jid, cc); 123 | } 124 | return result; 125 | }; 126 | } 127 | return target[p]; 128 | }, 129 | }); 130 | 131 | return Object.assign(async (value: I): Promise => { 132 | await op.send(value); 133 | return await collect(op); 134 | }, op); 135 | }; 136 | 137 | export const client = async (node: Libp2p, peer: PeerAddr | PeerAddr[]) => { 138 | // const { isMultiaddr, multiaddr } = await import("@multiformats/multiaddr"); 139 | // const { } = await import("@libp2p/interface/peer-id"); 140 | // const { peerIdFromString } = await import("@libp2p/peer-id"); 141 | let peerToDail: Multiaddr | PeerId; 142 | if (!(peer instanceof Array)) { 143 | peer = [peer]; 144 | } 145 | for (const item of peer) { 146 | try { 147 | if (!(isPeerId(item) || isMultiaddr(item))) { 148 | try { 149 | const addr = multiaddr(item); 150 | if (typeof addr.getPeerId() === "string") { 151 | peerToDail = addr; 152 | } else { 153 | throw runtimeError( 154 | "ConnectError", 155 | "Invalid addr: no peerid in addr", 156 | ); 157 | } 158 | } catch (e) { 159 | try { 160 | const peerid = peerIdFromString(item); 161 | peerToDail = peerid; 162 | } catch (error) { 163 | throw runtimeError("ConnectError", "Invalid peerid: " + item); 164 | } 165 | } 166 | } else { 167 | peerToDail = item; 168 | } 169 | 170 | logger.trace(`Try to connect to ${peerToDail}`); 171 | const connection = await node.dial(peerToDail); 172 | logger.trace(`Succeed connect to ${peerToDail}`); 173 | 174 | const controlChan = await channel( 175 | control_name, 176 | connection, 177 | { 178 | codec: defaultCodec, 179 | }, 180 | ); 181 | 182 | // collect error 183 | consume(transform( 184 | Infinity, 185 | async (i) => { 186 | const msg: ControlMsg = JSON.parse(i); 187 | const cc = ccs.get(JSON.stringify(msg.id)); 188 | await cc?.push(msg); 189 | logger.trace( 190 | `Send control msg ${JSON.stringify(msg)} to ${ 191 | JSON.stringify(msg.id) 192 | }`, 193 | ); 194 | }, 195 | controlChan, 196 | )); 197 | 198 | return Object.assign( 199 | async ( 200 | name: string, 201 | options?: InitOptions, 202 | ) => { 203 | const runtimeOptions = { 204 | ...defaultInitOptions, 205 | ...options, 206 | }; 207 | let chan = await channel( 208 | name, 209 | connection, 210 | runtimeOptions, 211 | ); 212 | // auto reopen 213 | return new Proxy(chan, { 214 | async apply(_, __, argArray) { 215 | if (!chan.stat.open()) { 216 | chan = await channel(name, connection, runtimeOptions); 217 | } 218 | return await chan(argArray.at(0)); 219 | }, 220 | get(_, p) { 221 | if (p === "send") { 222 | return async (v: I) => { 223 | if (!chan.stat.open()) { 224 | chan = await channel( 225 | name, 226 | connection, 227 | runtimeOptions, 228 | ); 229 | } 230 | await chan.send(v); 231 | }; 232 | } 233 | return chan[p]; 234 | }, 235 | }); 236 | }, 237 | { 238 | close: () => connection.close(), 239 | }, 240 | ); 241 | } catch (e) { 242 | logger.trace(`Skip addr ${item} because of ${e}`); 243 | continue; 244 | } 245 | } 246 | throw runtimeError("ConnectError", "cannot connect to " + peer); 247 | }; 248 | -------------------------------------------------------------------------------- /packages/libp2p-transport/src/codec.ts: -------------------------------------------------------------------------------- 1 | import { decode, encode } from "@xobj/core"; 2 | import { Codec } from "./types"; 3 | 4 | export const defaultCodec: Codec = { 5 | decoder: (array) => 6 | decode( 7 | array.buffer.slice(array.byteOffset, array.byteLength + array.byteOffset), 8 | ), 9 | encoder: (data) => new Uint8Array(encode(data)), 10 | }; 11 | -------------------------------------------------------------------------------- /packages/libp2p-transport/src/common.ts: -------------------------------------------------------------------------------- 1 | import { defaultCodec } from "./codec"; 2 | import type { ServerInitOptions } from "./types"; 3 | 4 | export const defaultInitOptions: Omit< 5 | Required< 6 | ServerInitOptions 7 | >, 8 | "middleware" 9 | > = { 10 | codec: defaultCodec, 11 | context: {}, 12 | }; 13 | 14 | export const control_name = "libp2p-transport/control"; 15 | -------------------------------------------------------------------------------- /packages/libp2p-transport/src/const.ts: -------------------------------------------------------------------------------- 1 | const debug = false; 2 | 3 | export { debug }; 4 | -------------------------------------------------------------------------------- /packages/libp2p-transport/src/error.ts: -------------------------------------------------------------------------------- 1 | type runtimeError = "ConnectError" | "ChannelNotFound"; 2 | 3 | export const runtimeError = (type: runtimeError, msg: string) => { 4 | const err = new Error(msg); 5 | err.name = type; 6 | return err; 7 | }; 8 | -------------------------------------------------------------------------------- /packages/libp2p-transport/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./client"; 2 | export * from "./server"; 3 | export * from "./types"; 4 | -------------------------------------------------------------------------------- /packages/libp2p-transport/src/logger.ts: -------------------------------------------------------------------------------- 1 | import pino from "pino"; 2 | import { debug } from "./const"; 3 | 4 | export const logger = pino(); 5 | 6 | if (debug) logger.level = "trace"; 7 | -------------------------------------------------------------------------------- /packages/libp2p-transport/src/server.ts: -------------------------------------------------------------------------------- 1 | import type { Libp2p } from "libp2p"; 2 | import type { 3 | Chan, 4 | Codec, 5 | ControlMsg, 6 | Func, 7 | IterableFunc, 8 | ServerInitOptions, 9 | Service, 10 | StreamID, 11 | } from "./types"; 12 | import { consume, transform } from "streaming-iterables"; 13 | import { control_name, defaultInitOptions } from "./common"; 14 | import { Channel } from "queueable"; 15 | import { makeNext, newChannel } from "./utils"; 16 | import { runtimeError } from "./error"; 17 | import { defaultCodec } from "./codec"; 18 | import { logger } from "./logger"; 19 | 20 | const ccs = new Map>(); 21 | 22 | export const server = async (node: Libp2p) => { 23 | const makeHandleStream = 24 | (codec: Codec) => 25 | async ( 26 | name: string, 27 | func: IterableFunc, 28 | ) => 29 | await node.handle(name, async (incomingData) => { 30 | // decode input 31 | const inputIterator = transform( 32 | Infinity, 33 | async (data) => await codec.decoder(data.subarray()) as Awaited, 34 | incomingData.stream.source, 35 | ); 36 | // process func 37 | const outputIterator = transform( 38 | Infinity, 39 | (data) => codec.encoder(data), 40 | await func(inputIterator, incomingData), 41 | ); 42 | // return output 43 | incomingData.stream.sink(outputIterator); 44 | }); 45 | 46 | const serve = async < 47 | I extends T, 48 | O extends T, 49 | T = any, 50 | Context extends {} = {}, 51 | >( 52 | name: string, 53 | func: Service, 54 | options?: ServerInitOptions, 55 | ) => { 56 | const runtimeOptions = { 57 | ...defaultInitOptions, 58 | ...options, 59 | }; 60 | await makeHandleStream(runtimeOptions.codec)( 61 | name, 62 | async (input, incomingData) => { 63 | const outputChannel = new Channel(); 64 | let chan!: Chan; 65 | 66 | // first msg is id, use id to make chan 67 | for await (const id of input) { 68 | const sid: StreamID = JSON.parse(id as string); 69 | // sync the id 70 | incomingData.connection.id = sid.connection; 71 | incomingData.stream.id = sid.stream; 72 | const cc = ccs.get(sid.connection); 73 | if (cc === undefined && name !== control_name) { 74 | throw runtimeError("ChannelNotFound", "Control channel not found"); 75 | } 76 | chan = newChannel( 77 | outputChannel, 78 | incomingData, 79 | cc, 80 | runtimeOptions.context, 81 | ); 82 | logger.trace(`New connection with ${id}`); 83 | break; 84 | } 85 | 86 | try { 87 | const process: Func = 88 | (await func(chan) ?? (() => {})) as Func; 89 | // transform input 90 | consume( 91 | transform( 92 | Infinity, 93 | async (data) => { 94 | logger.trace(`Incoming data: ${JSON.stringify(data)}`); 95 | try { 96 | if (runtimeOptions.middleware) { 97 | await runtimeOptions.middleware({ 98 | data, 99 | ...chan, 100 | next: makeNext({ data, ...chan, next: process }), 101 | }); 102 | } else { 103 | await process({ data, ...chan }); 104 | } 105 | } catch (error) { 106 | await chan.done(error); 107 | } 108 | }, 109 | input, 110 | ), 111 | ).then(() => chan?.done()); 112 | } catch (error) { 113 | await chan?.done(error); 114 | } 115 | return outputChannel; 116 | }, 117 | ); 118 | }; 119 | 120 | const handle = async < 121 | I extends T, 122 | O extends T, 123 | T = any, 124 | Context extends {} = {}, 125 | >( 126 | name: string, 127 | func: Func, 128 | options?: ServerInitOptions, 129 | ) => await serve(name, () => func, options); 130 | 131 | // collect status and send them to client 132 | if (!node.getProtocols().some((p) => p === control_name)) { 133 | await serve(control_name, (chan) => { 134 | const id = chan.id.connection; 135 | const cc = new Channel(); 136 | ccs.set(id, cc); 137 | consume( 138 | transform(Infinity, async (i) => { 139 | try { 140 | await chan.send(JSON.stringify(i)); 141 | } catch (error) { 142 | if (error === "channel has already closed") { 143 | // connection has already been closed forcely 144 | await cc.return(); 145 | ccs.delete(id); 146 | } else throw error; 147 | } 148 | }, cc), 149 | ); 150 | }, { 151 | codec: defaultCodec, 152 | }); 153 | } 154 | 155 | return { handle, serve }; 156 | }; 157 | -------------------------------------------------------------------------------- /packages/libp2p-transport/src/types.ts: -------------------------------------------------------------------------------- 1 | import type { AnyIterable } from "streaming-iterables"; 2 | import type { Multiaddr } from "@multiformats/multiaddr"; 3 | import type { PeerId, IncomingStreamData } from "@libp2p/interface"; 4 | import type { newChannel } from "./utils"; 5 | 6 | export type Chan = ReturnType>; 7 | 8 | export type IterableFunc = ( 9 | data: AnyIterable, 10 | incomingData: IncomingStreamData, 11 | ) => AnyIterable | Promise>; 12 | 13 | export type PeerAddr = string | Multiaddr | PeerId; 14 | 15 | export interface FuncParams extends Chan { 16 | data: I; 17 | } 18 | 19 | export interface MiddlewareParams 20 | extends FuncParams { 21 | next: (params?: Partial>) => Promise; 22 | } 23 | 24 | export type Func = ( 25 | params: FuncParams, 26 | ) => Promise | void; 27 | 28 | export type Service = ( 29 | chan: Chan, 30 | ) => Promise> | Func | void | Promise; 31 | 32 | export interface Codec { 33 | encoder: (data: T) => Uint8Array | Promise; 34 | decoder: (data: Uint8Array) => T | Promise; 35 | } 36 | 37 | export interface InitOptions { 38 | // custom codec 39 | codec?: Codec; 40 | context?: S; 41 | } 42 | 43 | export interface ServerInitOptions 44 | extends InitOptions { 45 | middleware?: ( 46 | params: MiddlewareParams, 47 | ) => void | Promise; 48 | } 49 | 50 | export interface TransportChannel 51 | extends AsyncIterableIterator, Chan {} 52 | 53 | export interface MetaPointError extends Error { 54 | type: "error"; 55 | id: StreamID; 56 | } 57 | 58 | export interface MetaPointSuccess { 59 | type: "success"; 60 | id: StreamID; 61 | } 62 | 63 | export type ControlMsg = MetaPointError | MetaPointSuccess; 64 | 65 | export interface StreamID { 66 | connection: string; 67 | stream: string; 68 | } 69 | -------------------------------------------------------------------------------- /packages/libp2p-transport/src/utils.ts: -------------------------------------------------------------------------------- 1 | import type { IncomingStreamData } from "@libp2p/interface"; 2 | import type { Channel } from "queueable"; 3 | import { debug } from "./const"; 4 | import { logger } from "./logger"; 5 | import type { ControlMsg, Func, FuncParams, StreamID } from "./types"; 6 | 7 | const newChannel = ( 8 | c: Channel, 9 | i: IncomingStreamData, 10 | ctrl?: Channel, 11 | context?: S, 12 | ) => { 13 | const id: StreamID = { connection: i.connection.id, stream: i.stream.id }; 14 | let open = true; 15 | return { 16 | send: async (value: T) => { 17 | if (!open) { 18 | throw "channel has already closed"; 19 | } 20 | await c.push(value); 21 | logger.trace(`Send ${JSON.stringify(value)} to ${JSON.stringify(id)}`); 22 | }, 23 | done: async (err?: Error) => { 24 | await c.return(); 25 | if (ctrl) { 26 | if (err) { 27 | await ctrl.push({ 28 | type: "error", 29 | id, 30 | name: err.name, 31 | message: err.message, 32 | stack: debug ? err.stack : undefined, 33 | }); 34 | logger.debug(`${JSON.stringify(id)} chan done with error`); 35 | } else { 36 | await ctrl.push({ 37 | type: "success", 38 | id, 39 | }); 40 | logger.trace(`${JSON.stringify(id)} chan done with success`); 41 | } 42 | } 43 | open = false; 44 | }, 45 | id, 46 | protocol: i.stream.protocol, 47 | context: context as S, 48 | stat: { 49 | remoteAddr: i.connection.remoteAddr, 50 | remotePeer: i.connection.remotePeer, 51 | encryption: i.connection.encryption, 52 | multiplexer: i.connection.multiplexer, 53 | direction: i.stream.direction, 54 | open: () => open, 55 | }, 56 | }; 57 | }; 58 | 59 | const makeNext = ( 60 | defaultParams: FuncParams & { next: Func }, 61 | ) => 62 | async (params?: Partial>) => { 63 | const { next, ...pp } = defaultParams; 64 | 65 | await next({ 66 | ...pp, 67 | ...params, 68 | }); 69 | }; 70 | 71 | export { makeNext, newChannel }; 72 | -------------------------------------------------------------------------------- /packages/libp2p-transport/test/handler.ts: -------------------------------------------------------------------------------- 1 | import { Func, server, Service } from "../src"; 2 | import { jsonCodec } from "./jsonCodec"; 3 | import type { Data } from "./types"; 4 | import { newNode } from "./node"; 5 | 6 | const repeatingService: Service = async (chan) => { 7 | const { send, done } = chan; 8 | let num: number = 0; 9 | let task: NodeJS.Timeout; 10 | 11 | const func: Func = async ({ data }) => { 12 | num = data; 13 | clearInterval(task); 14 | task = setInterval(() => send(num), 1000); 15 | }; 16 | return func; 17 | }; 18 | 19 | export const startServer = async () => { 20 | const libp2p = await newNode(); 21 | 22 | // default codec 23 | const { handle, serve } = await server(libp2p); 24 | 25 | // test context and middleware 26 | await handle( 27 | "add", 28 | async ({ data, send, done, context }) => { 29 | await send(data + 1 - context.other); 30 | await done(); 31 | }, 32 | { 33 | middleware: async (params) => { 34 | await params.next({ 35 | data: params.data + 1, 36 | }); 37 | }, 38 | context: { other: 1 }, 39 | }, 40 | ); 41 | 42 | await handle( 43 | "error", 44 | async () => { 45 | throw new Error("some error"); 46 | }, 47 | ); 48 | 49 | await handle( 50 | "adding", 51 | async ({ data, send, done }) => { 52 | await send(data + 1); 53 | await send(data + 2); 54 | await send(data + 3); 55 | await done(); 56 | }, 57 | ); 58 | 59 | // Infinity push handler 60 | await serve( 61 | "repeating", 62 | repeatingService, 63 | ); 64 | 65 | await serve( 66 | "channelAdd", 67 | () => ({ data, send }) => send(data + 1), 68 | ); 69 | 70 | // json codec 71 | const json = await server(libp2p); 72 | 73 | await json.handle("addJson", async ({ data, send, done }) => { 74 | await send({ value: data.value + 1 }); 75 | await done(); 76 | }, { codec: jsonCodec }); 77 | 78 | await json.handle( 79 | "addingJson", 80 | async ({ data, send, done }) => { 81 | await send({ value: data.value + 1 }); 82 | await send({ value: data.value + 2 }); 83 | await send({ value: data.value + 3 }); 84 | await done(); 85 | }, 86 | { codec: jsonCodec }, 87 | ); 88 | 89 | return libp2p.getMultiaddrs()[0]; 90 | }; 91 | -------------------------------------------------------------------------------- /packages/libp2p-transport/test/http.ts: -------------------------------------------------------------------------------- 1 | import fastify from "fastify"; 2 | export const startHttp = async () => { 3 | const server = fastify(); 4 | 5 | // Declare a route 6 | server.get("/:id", async (request) => { 7 | return Number((request.params as any)["id"]) + 1; 8 | }); 9 | 10 | // Run the server! 11 | await server.listen({ port: 3000 }); 12 | }; 13 | -------------------------------------------------------------------------------- /packages/libp2p-transport/test/index.bench.ts: -------------------------------------------------------------------------------- 1 | import { client } from "../src"; 2 | import { bench, describe } from "vitest"; 3 | import { startServer } from "./handler"; 4 | import { jsonCodec } from "./jsonCodec"; 5 | import type { Data } from "./types"; 6 | import { startHttp } from "./http"; 7 | import { newNode } from "./node"; 8 | 9 | const libp2p = await newNode(); 10 | const addr = await startServer(); 11 | let num = Math.floor(Math.random() * 100); 12 | 13 | const defaultClient = await client(libp2p, addr); 14 | const jsonClient = await client(libp2p, addr); 15 | const add = await defaultClient("add"); 16 | const ca = await defaultClient("channelAdd"); 17 | const c = await jsonClient("addJson", { codec: jsonCodec }); 18 | 19 | describe("json/xobj codec simple benchmark", async () => { 20 | bench("xobj codec", async () => { 21 | await add(num); 22 | }); 23 | 24 | bench("JSON codec", async () => { 25 | await c({ value: num }); 26 | }); 27 | }); 28 | 29 | describe("libp2p-transport/http simple benchmark", async () => { 30 | await startHttp(); 31 | const times = 200; 32 | bench("libp2p-transport", async () => { 33 | for (let index = 0; index < times; index++) { 34 | await add(num); 35 | } 36 | }); 37 | 38 | bench("libp2p-transport(channel)", async () => { 39 | for (let index = 0; index < times; index++) { 40 | await ca.send(num); 41 | } 42 | for (let index = 0; index < times; index++) { 43 | await ca.next(); 44 | } 45 | }); 46 | 47 | bench("http", async () => { 48 | for (let index = 0; index < times; index++) { 49 | await (await fetch("http://localhost:3000/1")).json(); 50 | } 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /packages/libp2p-transport/test/index.test.ts: -------------------------------------------------------------------------------- 1 | import { client } from "../src"; 2 | import { describe, expect, test } from "vitest"; 3 | import { startServer } from "./handler"; 4 | import { jsonCodec } from "./jsonCodec"; 5 | import type { Data } from "./types"; 6 | import { newNode } from "./node"; 7 | 8 | const libp2p = await newNode(); 9 | const addr = await startServer(); 10 | const defaultClient = await client(libp2p, addr); 11 | const jsonClient = await client(libp2p, addr); 12 | 13 | describe.concurrent("Server default codec", async () => { 14 | test.concurrent("test add handler(one2one)", async () => { 15 | let c = await defaultClient("add"); 16 | await c.send(2); 17 | let count = 0; 18 | for await (const msg of c) { 19 | expect(msg).toBe(3); 20 | count += 1; 21 | } 22 | expect(count).toBe(1); 23 | expect((await c.next()).value).toBe(undefined); 24 | expect(await c(3)).toStrictEqual([4]); 25 | await c.send(5); 26 | for await (const msg of c) { 27 | expect(msg).toBe(6); 28 | } 29 | }); 30 | 31 | test.concurrent("test error handler", async () => { 32 | try { 33 | const c = await defaultClient("error"); 34 | await c(); 35 | // this should never run 36 | expect(1).toBe(2); 37 | } catch (error) { 38 | expect(error).toBeInstanceOf(Error); 39 | } 40 | }); 41 | 42 | test.concurrent("test adding handler(one2many)", async () => { 43 | const c = await defaultClient("adding"); 44 | const my_num = Math.floor(Math.random() * 100); 45 | await c.send(my_num); 46 | let n = 1; 47 | for await (const msg of c) { 48 | expect(msg).toStrictEqual(my_num + n); 49 | n += 1; 50 | } 51 | expect(n).toStrictEqual(4); 52 | }); 53 | }); 54 | 55 | describe.concurrent("Server JSON codec", async () => { 56 | test.concurrent("test add handler(one2one)", async () => { 57 | const c = await jsonClient("addJson", { codec: jsonCodec }); 58 | await c.send({ value: 2 }); 59 | let count = 0; 60 | for await (const msg of c) { 61 | expect(msg).toStrictEqual({ value: 3 }); 62 | count += 1; 63 | } 64 | expect(count).toBe(1); 65 | expect((await c.next()).value).toBe(undefined); 66 | }); 67 | 68 | test.concurrent("test add handler(one2one)2", async () => { 69 | const c = await jsonClient("addJson", { codec: jsonCodec }); 70 | const result = await c({ value: 2 }); 71 | expect(result).toStrictEqual([{ value: 3 }]); 72 | expect((await c.next()).value).toBe(undefined); 73 | }); 74 | 75 | test.concurrent("test adding handler(one2many)", async () => { 76 | const c = await jsonClient("addingJson", { codec: jsonCodec }); 77 | const my_num = Math.floor(Math.random() * 100); 78 | await c.send({ value: my_num }); 79 | let n = 1; 80 | for await (const msg of c) { 81 | expect(msg).toStrictEqual({ value: my_num + n }); 82 | n += 1; 83 | } 84 | expect(n).toStrictEqual(4); 85 | }); 86 | }); 87 | 88 | describe("Infinity output service(one2Infinity)", async () => { 89 | const c = await defaultClient("repeating"); 90 | test("Infinity out with control", async () => { 91 | await c.send(1); 92 | let count = 0; 93 | for await (const _ of c) { 94 | count += 1; 95 | if (count === 2) { 96 | await c.done(); 97 | } 98 | } 99 | expect(count).toBe(2); 100 | }); 101 | }); 102 | -------------------------------------------------------------------------------- /packages/libp2p-transport/test/jsonCodec.ts: -------------------------------------------------------------------------------- 1 | import { stringToUint8Array, uint8ArrayToString } from "binconv"; 2 | import { Codec } from "../src"; 3 | import { Json } from "./types"; 4 | import destr from "destr"; 5 | 6 | export const jsonCodec: Codec = { 7 | encoder: (data) => stringToUint8Array(JSON.stringify(data)), 8 | decoder: (data) => destr(uint8ArrayToString(data)), 9 | }; 10 | -------------------------------------------------------------------------------- /packages/libp2p-transport/test/node.ts: -------------------------------------------------------------------------------- 1 | import { createLibp2p } from "libp2p"; 2 | import { mplex } from "@libp2p/mplex"; 3 | import { noise } from "@chainsafe/libp2p-noise"; 4 | import { webSockets } from "@libp2p/websockets"; 5 | import { all } from "@libp2p/websockets/filters"; 6 | 7 | export const newNode = async () => 8 | await createLibp2p({ 9 | transports: [ 10 | webSockets({ 11 | filter: all, 12 | }), 13 | ], 14 | streamMuxers: [mplex()], 15 | addresses: { 16 | listen: ["/ip4/0.0.0.0/tcp/0/ws"], 17 | }, 18 | connectionEncryption: [noise()], 19 | }); 20 | -------------------------------------------------------------------------------- /packages/libp2p-transport/test/types.ts: -------------------------------------------------------------------------------- 1 | export type Json = 2 | | string 3 | | number 4 | | boolean 5 | | null 6 | | { [key: string]: Json } 7 | | Json[]; 8 | export interface Data { 9 | [key: string]: Json; 10 | value: number; 11 | } 12 | -------------------------------------------------------------------------------- /packages/metapoint/README.md: -------------------------------------------------------------------------------- 1 | # MetaPoint 2 | 3 | Meta first and low-code. 4 | 5 | Peer-to-Peer typesafe APIs or Channels made easy. 6 | 7 | ## Intro 8 | 9 | MetaPoint allows you to easily build typesafe APIs or Channels wherever 10 | JavaScript runs. 11 | 12 | ## Quickstart 13 | 14 | ```typescript 15 | import { h, MetaType, peer, z } from "metapoint"; 16 | 17 | // router group 18 | const g = h(); 19 | 20 | // node1(nodejs/web/deno) 21 | const node1 = await peer({ 22 | endpoint: { 23 | numberAdd: g.handler({ 24 | func: async ({ data, send, done }) => { 25 | await send(data + 1); 26 | await done(); 27 | }, 28 | input: z.number(), 29 | output: z.number(), 30 | }), 31 | }, 32 | }); 33 | export type meta = MetaType; 34 | 35 | // node2(nodejs/web/deno) 36 | const node2 = await peer(); 37 | const channel = await node2.connect(node1.meta().addrs); 38 | const add = await channel("numberAdd"); 39 | console.log(await add(1)); // [2] 40 | ``` 41 | -------------------------------------------------------------------------------- /packages/metapoint/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "metapoint", 3 | "version": "0.1.5", 4 | "description": "⚡Meta first and low-code. Peer-to-Peer typesafe APIs or Channels made easy.", 5 | "type": "module", 6 | "main": "dist/index.cjs", 7 | "module": "dist/index.js", 8 | "types": "dist/index.d.ts", 9 | "exports": { 10 | "import": "./dist/index.js", 11 | "types": "./dist/index.d.ts", 12 | "require": "./dist/index.cts" 13 | }, 14 | "keywords": [ 15 | "libp2p", 16 | "channel", 17 | "zod", 18 | "low-code", 19 | "p2p", 20 | "subscription" 21 | ], 22 | "author": "sovlookup ", 23 | "license": "MIT", 24 | "homepage": "https://github.com/SOVLOOKUP/metapoint", 25 | "repository": { 26 | "type": "git", 27 | "url": "git+https://github.com/SOVLOOKUP/metapoint" 28 | }, 29 | "bugs": { 30 | "url": "https://github.com/SOVLOOKUP/metapoint/issues" 31 | }, 32 | "files": [ 33 | "dist/*", 34 | "README.md" 35 | ], 36 | "scripts": { 37 | "build": "tsup --cjsInterop && vite build", 38 | "test": "vitest run" 39 | }, 40 | "dependencies": { 41 | "@chainsafe/libp2p-noise": "^14.0.0", 42 | "@libp2p/mplex": "^10.0.5", 43 | "@libp2p/websockets": "^8.0.5", 44 | "@multiformats/multiaddr": "^12.1.11", 45 | "libp2p": "^1.0.8", 46 | "libp2p-transport": "workspace:*", 47 | "nanoid": "^5.0.4", 48 | "zod": "^3.22.4" 49 | }, 50 | "devDependencies": { 51 | "vite": "^5.0.5" 52 | }, 53 | "tsup": { 54 | "entry": [ 55 | "src/index.ts" 56 | ], 57 | "format": [ 58 | "cjs", 59 | "esm" 60 | ], 61 | "clean": true, 62 | "dts": true, 63 | "treeshake": true, 64 | "shims": true 65 | } 66 | } -------------------------------------------------------------------------------- /packages/metapoint/src/helper.ts: -------------------------------------------------------------------------------- 1 | import { ServerInitOptions } from "libp2p-transport"; 2 | import type { HandlerMeta, ServiceMeta, Unpick } from "./types"; 3 | 4 | export const h = ( 5 | opts?: ServerInitOptions, 6 | ) => { 7 | const { codec, context, middleware } = opts ?? {}; 8 | 9 | const handler = ( 10 | meta: Unpick, "type">, 11 | ) => { 12 | if (codec && meta.codec === undefined) meta.codec = codec; 13 | if (context && meta.context === undefined) meta.context = context as unknown as typeof meta["context"]; 14 | if (middleware && meta.middleware === undefined) { 15 | meta.middleware = middleware as unknown as typeof meta["middleware"]; 16 | } 17 | 18 | return { 19 | type: "handler" as const, 20 | ...meta, 21 | }; 22 | }; 23 | 24 | const service = ( 25 | meta: Unpick, "type">, 26 | ) => { 27 | if (codec && meta.codec === undefined) meta.codec = codec; 28 | if (context && meta.context === undefined) meta.context = context as unknown as typeof meta["context"]; 29 | if (middleware && meta.middleware === undefined) { 30 | meta.middleware = middleware as unknown as typeof meta["middleware"]; 31 | } 32 | 33 | return { 34 | type: "service" as const, 35 | ...meta, 36 | }; 37 | }; 38 | 39 | return { 40 | handler, 41 | service, 42 | }; 43 | }; 44 | -------------------------------------------------------------------------------- /packages/metapoint/src/index.ts: -------------------------------------------------------------------------------- 1 | import { client, InitOptions, PeerAddr, server } from "libp2p-transport"; 2 | import type { 3 | ConnectEndpoint, 4 | Endpoint, 5 | EndpointMeta, 6 | InferIOType, 7 | PeerInitOptions, 8 | UnPromisify, 9 | } from "./types"; 10 | import { newNode } from "./node"; 11 | 12 | const parseOptions = async ( 13 | options: PeerInitOptions, 14 | ) => ({ 15 | libp2p: options.libp2p ?? await newNode(), 16 | initStart: options.initStart ?? true, 17 | endpoint: options.endpoint ?? {}, 18 | ...options, 19 | }); 20 | 21 | export const peer = async >( 22 | options?: T, 23 | ) => { 24 | const { libp2p, initStart, endpoint } = await parseOptions(options ?? {}); 25 | const { handle, serve } = await server(libp2p); 26 | const metadata = new Map>(); 27 | 28 | const addEndpoint = async (endpoint: Endpoint) => { 29 | for (const [name, meta] of Object.entries(endpoint)) { 30 | switch (meta.type) { 31 | case "handler": 32 | await handle(name, meta.func, meta); 33 | metadata.set(name, meta); 34 | break; 35 | case "service": 36 | await serve(name, meta.func, meta); 37 | metadata.set(name, meta); 38 | break; 39 | default: 40 | break; 41 | } 42 | } 43 | }; 44 | 45 | const unhandle = async (...names: string[]) => 46 | await libp2p.unhandle( 47 | names.map((name: string) => { 48 | metadata.delete(name); 49 | return name; 50 | }), 51 | ); 52 | 53 | const connect = async < 54 | T extends ConnectEndpoint, 55 | Context extends {} = {}, 56 | >( 57 | peer: PeerAddr | PeerAddr[], 58 | options?: InitOptions, 59 | ) => { 60 | const channel = await client(libp2p, peer); 61 | return Object.assign(async (name: F) => 62 | await channel< 63 | InferIOType, 64 | InferIOType, 65 | any, 66 | Context 67 | >(name.toString(), options), { close: channel.close }); 68 | }; 69 | 70 | const getMeta = () => ({ 71 | addrs: libp2p.getMultiaddrs().map((d) => d.toString()), 72 | endpoint: Object.fromEntries(metadata.entries()) as T["endpoint"], 73 | }); 74 | 75 | if (initStart) { 76 | await addEndpoint(endpoint); 77 | await libp2p.start(); 78 | } 79 | 80 | return { 81 | meta: getMeta, 82 | handle: addEndpoint, 83 | unhandle, 84 | connect, 85 | }; 86 | }; 87 | 88 | export type MetaType>> = 89 | ReturnType["endpoint"]; 90 | 91 | export * from "./helper"; 92 | export { z } from "zod"; 93 | -------------------------------------------------------------------------------- /packages/metapoint/src/node.ts: -------------------------------------------------------------------------------- 1 | import { createLibp2p } from "libp2p"; 2 | import { mplex } from "@libp2p/mplex"; 3 | import { noise } from "@chainsafe/libp2p-noise"; 4 | import { webSockets } from "@libp2p/websockets"; 5 | import { all } from "@libp2p/websockets/filters"; 6 | 7 | export const newNode = async () => 8 | await createLibp2p({ 9 | transports: [ 10 | webSockets({ 11 | filter: all, 12 | }), 13 | ], 14 | streamMuxers: [mplex()], 15 | addresses: { 16 | listen: typeof window === "object" ? [] : ["/ip4/0.0.0.0/tcp/0/ws"], 17 | }, 18 | connectionEncryption: [noise()], 19 | }); 20 | -------------------------------------------------------------------------------- /packages/metapoint/src/types.ts: -------------------------------------------------------------------------------- 1 | import type { Func, ServerInitOptions, Service } from "libp2p-transport"; 2 | import type { Libp2p } from "libp2p"; 3 | import type { z, ZodType } from "zod"; 4 | 5 | type IsEqual = (() => T1 extends T ? 1 : 2) extends 6 | (() => T2 extends U ? 1 : 2) ? true 7 | : false; 8 | 9 | export type InferIOType = 10 | IsEqual extends true ? T : z.infer>; 11 | 12 | export type HandlerFunc = Func< 13 | InferIOType, I>, 14 | InferIOType, O>, 15 | S 16 | >; 17 | 18 | export type ServiceFunc = Service< 19 | InferIOType, I>, 20 | InferIOType, O>, 21 | S 22 | >; 23 | 24 | export interface MetaBase { 25 | info?: Json; 26 | input?: ZodType; 27 | output?: ZodType; 28 | } 29 | 30 | interface Meta 31 | extends MetaBase, ServerInitOptions {} 32 | 33 | type Simplify = { 34 | [P in keyof T]: T[P]; 35 | }; 36 | 37 | export type Unpick = Simplify< 38 | Pick> 39 | >; 40 | 41 | export interface HandlerMeta 42 | extends Meta { 43 | type: "handler"; 44 | func: HandlerFunc; 45 | } 46 | 47 | export interface ServiceMeta 48 | extends Meta { 49 | type: "service"; 50 | func: ServiceFunc; 51 | } 52 | 53 | export type EndpointMeta = 54 | | HandlerMeta 55 | | ServiceMeta; 56 | 57 | export interface Endpoint { 58 | [name: string]: EndpointMeta; 59 | } 60 | 61 | export interface ConnectEndpoint { 62 | [name: string]: MetaBase; 63 | } 64 | 65 | export interface PeerInitOptions { 66 | endpoint?: Endpoint; 67 | libp2p?: Libp2p; 68 | initStart?: boolean; 69 | } 70 | 71 | export type UnPromisify = T extends Promise ? U : never; 72 | 73 | export type Json = 74 | | string 75 | | number 76 | | boolean 77 | | null 78 | | { [key: string]: Json } 79 | | Json[]; 80 | -------------------------------------------------------------------------------- /packages/metapoint/test/index.test.ts: -------------------------------------------------------------------------------- 1 | import { h, peer, z } from "../src"; 2 | import { describe, expect, test } from "vitest"; 3 | 4 | const g = h({ 5 | context: { name: 1 }, 6 | middleware: ({ context, next }) => next({ data: context.name + 1 }), 7 | }); 8 | 9 | const endpoint = { 10 | numberAdd: g.handler({ 11 | func: async ({ data, send, done }) => { 12 | await send(data); 13 | await done(); 14 | }, 15 | input: z.number(), 16 | output: z.number(), 17 | }), 18 | addChan: g.service({ 19 | func: () => async ({ data, send }) => { 20 | await send(data); 21 | }, 22 | input: z.number(), 23 | output: z.number(), 24 | }), 25 | }; 26 | 27 | const node1 = await peer({ endpoint }); 28 | const node2 = await peer(); 29 | const channel = await node2.connect(node1.meta().addrs); 30 | 31 | describe("test metapoint server/client", async () => { 32 | test("number test", async () => { 33 | const n = await channel("numberAdd"); 34 | expect(await n(1)).toStrictEqual([2]); 35 | }); 36 | test("string add chan", async () => { 37 | const c = await channel("addChan"); 38 | await c.send(1); 39 | expect((await c.next()).value).toBe(2); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /packages/metapoint/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | 3 | export default defineConfig({ 4 | build: { 5 | emptyOutDir: false, 6 | target: "esnext", 7 | lib: { 8 | entry: 'src/index.ts', 9 | formats: ["es"], 10 | }, 11 | } 12 | }) -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'packages/*' 3 | # exclude packages that are inside test directories 4 | - '!**/test/**' 5 | - 'examples/*' 6 | - 'docs' -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "ESNext", 4 | "target": "ESNext", 5 | "moduleResolution": "node" 6 | } 7 | } 8 | --------------------------------------------------------------------------------