├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── components.d.ts ├── env.d.ts ├── instantdb-license.md ├── package.json ├── sandbox ├── nuxt │ ├── .env.example │ ├── .gitignore │ ├── README.md │ ├── app.vue │ ├── components │ │ ├── ClientOnlyTodo.vue │ │ └── Todo.vue │ ├── composables │ │ └── instant.ts │ ├── instant.perms.ts │ ├── instant.schema.ts │ ├── nuxt.config.ts │ ├── package-lock.json │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ └── robots.txt │ ├── server │ │ └── tsconfig.json │ └── tsconfig.json └── vite-vue │ ├── .env.example │ ├── .gitignore │ ├── README copy.md │ ├── README.md │ ├── env.d.ts │ ├── index.html │ ├── instant.perms.ts │ ├── instant.schema.ts │ ├── package-lock.json │ ├── package.json │ ├── postcss.config.js │ ├── src │ ├── App.vue │ ├── components │ │ ├── BottomNav.vue │ │ ├── ErrorBoundary.vue │ │ ├── Header.vue │ │ ├── ThemeController.vue │ │ ├── TodoFooter.vue │ │ ├── TodoForm.vue │ │ ├── TodoList.vue │ │ └── UserSample.vue │ ├── db │ │ ├── composables.ts │ │ ├── index.ts │ │ └── todo.ts │ ├── index.css │ ├── main.ts │ ├── router.ts │ ├── utils │ │ └── composables.ts │ ├── views │ │ ├── Cursors.vue │ │ ├── CursorsIframe.vue │ │ ├── Docs.vue │ │ ├── Rooms.vue │ │ ├── Signin.vue │ │ ├── Todo.vue │ │ ├── Topics.vue │ │ ├── TopicsIframe.vue │ │ ├── Typing.vue │ │ └── TypingIframe.vue │ └── vite-env.d.ts │ ├── tailwind.config.js │ ├── tsconfig.app.json │ ├── tsconfig.json │ ├── tsconfig.node.json │ ├── tsconfig.paths.json │ └── vite.config.ts ├── scripts └── prepublish.js ├── src ├── InstantVueAbstractDatabase.ts ├── InstantVueRoom.ts ├── InstantVueWebDatabase.ts ├── __types__ │ ├── typeUtils.ts │ └── typesTest.ts ├── components │ ├── Cursor.vue │ ├── Cursors.vue │ └── index.ts ├── index.ts ├── init.ts ├── useQuery.ts ├── useTimeout.ts ├── utils.ts └── version.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | tmp 4 | dist 5 | .cache 6 | .DS_Store 7 | .idea 8 | *.log 9 | *.tgz 10 | coverage 11 | dist 12 | lib-cov 13 | logs 14 | temp 15 | .vscode -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | sandbox 2 | dev 3 | tmp -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright 2024 dorilama 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # instantdb-vue 2 | 3 | Unofficial port of [@instantdb/react](https://github.com/instantdb/instant/blob/main/client/packages/react/README.md) for vue 4 | 5 | ``` 6 | // ༼ つ ◕_◕ ༽つ Real-time Chat 7 | // ---------------------------------- 8 | // * Updates instantly 9 | // * Multiplayer 10 | // * Works offline 11 | 27 | 33 | ``` 34 | 35 | The aim of this library is to have @instantdb/react for vue with feature parity. You should be able to follow the react docs and examples using this library. Arguments are reactive so you can use refs or getters. `Cursors` component has a separate import from `@dorilama/instantdb-vue/components`. Some of the functions return also a `stop` function to manually clear all side-effects and break live updated. 36 | 37 | --- 38 | 39 | Related: [this](https://github.com/Dorilama/instantdb-byos#readme) library tries to bring the same sdk for multiple signal based reactivity. 40 | -------------------------------------------------------------------------------- /components.d.ts: -------------------------------------------------------------------------------- 1 | export * from './dist/components' 2 | -------------------------------------------------------------------------------- /env.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import type { DefineComponent } from 'vue' 3 | 4 | const component: DefineComponent, Record, any> 5 | export default component 6 | } 7 | -------------------------------------------------------------------------------- /instantdb-license.md: -------------------------------------------------------------------------------- 1 | -- 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 [yyyy] [name of copyright owner] 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. 204 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@dorilama/instantdb-vue", 3 | "version": "0.9.0", 4 | "description": "Unofficial Instant DB for Vue", 5 | "author": "dorilama", 6 | "license": "ISC", 7 | "exports": { 8 | ".": { 9 | "types": "./dist/index.d.ts", 10 | "import": "./dist/index.mjs", 11 | "require": "./dist/index.js" 12 | }, 13 | "./components": { 14 | "types": "./dist/components/index.d.ts", 15 | "import": "./dist/components/index.mjs", 16 | "require": "./dist/components/index.js" 17 | } 18 | }, 19 | "main": "./dist/index.js", 20 | "module": "./dist/index.mjs", 21 | "types": "./dist/index.d.ts", 22 | "typesVersions": { 23 | "*": { 24 | "*": [ 25 | "*", 26 | "dist/*", 27 | "dist/*.d.ts", 28 | "dist/*/index.d.ts" 29 | ] 30 | } 31 | }, 32 | "files": [ 33 | "components.d.ts", 34 | "dist" 35 | ], 36 | "homepage": "https://github.com/Dorilama/instantdb-vue#readme", 37 | "repository": { 38 | "type": "git", 39 | "url": "git+https://github.com/Dorilama/instantdb-vue.git" 40 | }, 41 | "scripts": { 42 | "build": "unbuild", 43 | "dev": "cd ./sandbox/vite-vue && npm run dev", 44 | "dev:nuxt": "cd ./sandbox/nuxt && npm run dev", 45 | "publish-package": "npm publish --access public", 46 | "prepublishOnly": "node ./scripts/prepublish.js && npm run build", 47 | "update": "npm i @instantdb/core@latest" 48 | }, 49 | "dependencies": { 50 | "@instantdb/core": "^0.22.84", 51 | "eventsource": "^4.0.0" 52 | }, 53 | "peerDependencies": { 54 | "vue": "^3.0.0-0" 55 | }, 56 | "devDependencies": { 57 | "@vue/tsconfig": "^0.5.1", 58 | "mkdist": "^2.2.0", 59 | "typescript": "^5.5.4", 60 | "unbuild": "^3.5.0" 61 | }, 62 | "type": "module" 63 | } 64 | -------------------------------------------------------------------------------- /sandbox/nuxt/.env.example: -------------------------------------------------------------------------------- 1 | INSTANT_APP_ID="" -------------------------------------------------------------------------------- /sandbox/nuxt/.gitignore: -------------------------------------------------------------------------------- 1 | # Nuxt dev/build outputs 2 | .output 3 | .data 4 | .nuxt 5 | .nitro 6 | .cache 7 | dist 8 | 9 | # Node dependencies 10 | node_modules 11 | 12 | # Logs 13 | logs 14 | *.log 15 | 16 | # Misc 17 | .DS_Store 18 | .fleet 19 | .idea 20 | 21 | # Local env files 22 | .env 23 | .env.* 24 | !.env.example 25 | -------------------------------------------------------------------------------- /sandbox/nuxt/README.md: -------------------------------------------------------------------------------- 1 | # Nuxt Minimal Starter 2 | 3 | Look at the [Nuxt documentation](https://nuxt.com/docs/getting-started/introduction) to learn more. 4 | 5 | ## Setup 6 | 7 | Make sure to install dependencies: 8 | 9 | ```bash 10 | # npm 11 | npm install 12 | 13 | # pnpm 14 | pnpm install 15 | 16 | # yarn 17 | yarn install 18 | 19 | # bun 20 | bun install 21 | ``` 22 | 23 | ## Development Server 24 | 25 | Start the development server on `http://localhost:3000`: 26 | 27 | ```bash 28 | # npm 29 | npm run dev 30 | 31 | # pnpm 32 | pnpm dev 33 | 34 | # yarn 35 | yarn dev 36 | 37 | # bun 38 | bun run dev 39 | ``` 40 | 41 | ## Production 42 | 43 | Build the application for production: 44 | 45 | ```bash 46 | # npm 47 | npm run build 48 | 49 | # pnpm 50 | pnpm build 51 | 52 | # yarn 53 | yarn build 54 | 55 | # bun 56 | bun run build 57 | ``` 58 | 59 | Locally preview production build: 60 | 61 | ```bash 62 | # npm 63 | npm run preview 64 | 65 | # pnpm 66 | pnpm preview 67 | 68 | # yarn 69 | yarn preview 70 | 71 | # bun 72 | bun run preview 73 | ``` 74 | 75 | Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information. 76 | -------------------------------------------------------------------------------- /sandbox/nuxt/app.vue: -------------------------------------------------------------------------------- 1 | 18 | 24 | -------------------------------------------------------------------------------- /sandbox/nuxt/components/ClientOnlyTodo.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 26 | -------------------------------------------------------------------------------- /sandbox/nuxt/components/Todo.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 30 | -------------------------------------------------------------------------------- /sandbox/nuxt/composables/instant.ts: -------------------------------------------------------------------------------- 1 | import { init as instantInit } from "@dorilama/instantdb-vue"; 2 | import type { InstaQLParams, InstaQLResult } from "@dorilama/instantdb-vue"; 3 | import schema from "../instant.schema"; 4 | 5 | const todosQuery = { todos: {} } satisfies InstaQLParams; 6 | 7 | type TodosResult = InstaQLResult; 8 | 9 | export type Todo = TodosResult["todos"][number]; 10 | 11 | function init() { 12 | const config = useRuntimeConfig(); 13 | const appId = config.public.instantAppId as string; 14 | return instantInit({ 15 | appId, 16 | __extra_vue: { clientOnlyUseQuery: true }, 17 | schema, 18 | }); 19 | } 20 | 21 | let db: ReturnType; 22 | 23 | export function useDb() { 24 | if (!db) { 25 | db = init(); 26 | } 27 | 28 | return db; 29 | } 30 | 31 | export const useClientOnlyQuery: typeof db.useQuery = (query) => { 32 | const db = useDb(); 33 | if (typeof window === "undefined") { 34 | return { 35 | isLoading: ref(true), 36 | data: shallowRef(undefined), 37 | pageInfo: shallowRef(undefined), 38 | error: shallowRef(undefined), 39 | stop: () => {}, 40 | }; 41 | } 42 | return db.useQuery(query); 43 | }; 44 | -------------------------------------------------------------------------------- /sandbox/nuxt/instant.perms.ts: -------------------------------------------------------------------------------- 1 | // Docs: https://www.instantdb.com/docs/permissions 2 | 3 | import type { InstantRules } from "@instantdb/core"; 4 | 5 | const rules = { 6 | /** 7 | * Welcome to Instant's permission system! 8 | * Right now your rules are empty. To start filling them in, check out the docs: 9 | * https://www.instantdb.com/docs/permissions 10 | * 11 | * Here's an example to give you a feel: 12 | * posts: { 13 | * allow: { 14 | * view: "true", 15 | * create: "isOwner", 16 | * update: "isOwner", 17 | * delete: "isOwner", 18 | * }, 19 | * bind: ["isOwner", "data.creator == auth.uid"], 20 | * }, 21 | */ 22 | docs: { 23 | allow: { 24 | view: "data.id == ruleParams.knownDocId", 25 | }, 26 | }, 27 | } satisfies InstantRules; 28 | 29 | export default rules; 30 | -------------------------------------------------------------------------------- /sandbox/nuxt/instant.schema.ts: -------------------------------------------------------------------------------- 1 | // Docs: https://www.instantdb.com/docs/modeling-data 2 | 3 | import { i } from "@instantdb/core"; 4 | 5 | const _schema = i.schema({ 6 | entities: { 7 | $files: i.entity({ 8 | path: i.string().unique().indexed(), 9 | url: i.string(), 10 | }), 11 | $users: i.entity({ 12 | email: i.string().unique().indexed().optional(), 13 | }), 14 | docs: i.entity({ 15 | text: i.string(), 16 | }), 17 | notes: i.entity({ 18 | createdAt: i.any().optional(), 19 | text: i.string().optional(), 20 | }), 21 | todos: i.entity({ 22 | createdAt: i.number(), 23 | done: i.boolean(), 24 | lastModified: i.date(), 25 | text: i.string(), 26 | textNew: i.string().optional(), 27 | }), 28 | user: i.entity({}), 29 | }, 30 | links: { 31 | notesTodos: { 32 | forward: { 33 | on: "notes", 34 | has: "many", 35 | label: "todos", 36 | }, 37 | reverse: { 38 | on: "todos", 39 | has: "one", 40 | label: "notes", 41 | }, 42 | }, 43 | }, 44 | rooms: { 45 | chat: { 46 | presence: i.entity({ 47 | color: i.string(), 48 | path: i.string(), 49 | userId: i.string(), 50 | }), 51 | topics: { 52 | emoji: i.entity({ 53 | color: i.string().optional(), 54 | text: i.string(), 55 | }), 56 | }, 57 | }, 58 | }, 59 | }); 60 | 61 | // This helps Typescript display nicer intellisense 62 | type _AppSchema = typeof _schema; 63 | interface AppSchema extends _AppSchema {} 64 | const schema: AppSchema = _schema; 65 | 66 | export type { AppSchema }; 67 | export default schema; 68 | -------------------------------------------------------------------------------- /sandbox/nuxt/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | 3 | // https://nuxt.com/docs/api/configuration/nuxt-config 4 | export default defineNuxtConfig({ 5 | compatibilityDate: "2024-04-03", 6 | devtools: { enabled: false }, 7 | runtimeConfig: { public: { instantAppId: process.env["INSTANT_APP_ID"] } }, 8 | alias: { 9 | "@dorilama/instantdb-vue": path.resolve(__dirname, "../../src"), 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /sandbox/nuxt/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nuxt-app", 3 | "private": true, 4 | "type": "module", 5 | "scripts": { 6 | "build": "nuxt build", 7 | "dev": "nuxt dev", 8 | "generate": "nuxt generate", 9 | "preview": "nuxt preview", 10 | "postinstall": "nuxt prepare" 11 | }, 12 | "dependencies": { 13 | "nuxt": "^4.2.2", 14 | "vue": "^3.0.0-0", 15 | "vue-router": "latest", 16 | "@dorilama/instantdb-vue": "file:../.." 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /sandbox/nuxt/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dorilama/instantdb-vue/HEAD/sandbox/nuxt/public/favicon.ico -------------------------------------------------------------------------------- /sandbox/nuxt/public/robots.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /sandbox/nuxt/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../.nuxt/tsconfig.server.json" 3 | } 4 | -------------------------------------------------------------------------------- /sandbox/nuxt/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // https://nuxt.com/docs/guide/concepts/typescript 3 | "extends": "./.nuxt/tsconfig.json", 4 | "compilerOptions": { 5 | "paths": { 6 | "@dorilama/instantdb-vue": ["../../src"] 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /sandbox/vite-vue/.env.example: -------------------------------------------------------------------------------- 1 | VITE_INSTANT_APP_ID="" -------------------------------------------------------------------------------- /sandbox/vite-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 | -------------------------------------------------------------------------------- /sandbox/vite-vue/README copy.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 ` 15 | 16 | 17 | -------------------------------------------------------------------------------- /sandbox/vite-vue/instant.perms.ts: -------------------------------------------------------------------------------- 1 | // Docs: https://www.instantdb.com/docs/permissions 2 | 3 | import type { InstantRules } from "@instantdb/core"; 4 | 5 | const rules = { 6 | /** 7 | * Welcome to Instant's permission system! 8 | * Right now your rules are empty. To start filling them in, check out the docs: 9 | * https://www.instantdb.com/docs/permissions 10 | * 11 | * Here's an example to give you a feel: 12 | * posts: { 13 | * allow: { 14 | * view: "true", 15 | * create: "isOwner", 16 | * update: "isOwner", 17 | * delete: "isOwner", 18 | * }, 19 | * bind: ["isOwner", "data.creator == auth.uid"], 20 | * }, 21 | */ 22 | docs: { 23 | allow: { 24 | view: "data.id == ruleParams.knownDocId", 25 | }, 26 | }, 27 | } satisfies InstantRules; 28 | 29 | export default rules; 30 | -------------------------------------------------------------------------------- /sandbox/vite-vue/instant.schema.ts: -------------------------------------------------------------------------------- 1 | // Docs: https://www.instantdb.com/docs/modeling-data 2 | 3 | import { i } from "@instantdb/core"; 4 | 5 | const _schema = i.schema({ 6 | entities: { 7 | $files: i.entity({ 8 | path: i.string().unique().indexed(), 9 | url: i.string(), 10 | }), 11 | $users: i.entity({ 12 | email: i.string().unique().indexed().optional(), 13 | }), 14 | docs: i.entity({ 15 | text: i.string(), 16 | }), 17 | notes: i.entity({ 18 | createdAt: i.any().optional(), 19 | text: i.string().optional(), 20 | }), 21 | todos: i.entity({ 22 | createdAt: i.number(), 23 | done: i.boolean(), 24 | lastModified: i.date(), 25 | text: i.string(), 26 | textNew: i.string().optional(), 27 | }), 28 | user: i.entity({}), 29 | }, 30 | links: { 31 | notesTodos: { 32 | forward: { 33 | on: "notes", 34 | has: "many", 35 | label: "todos", 36 | }, 37 | reverse: { 38 | on: "todos", 39 | has: "one", 40 | label: "notes", 41 | }, 42 | }, 43 | }, 44 | rooms: { 45 | chat: { 46 | presence: i.entity({ 47 | color: i.string(), 48 | path: i.string(), 49 | userId: i.string(), 50 | }), 51 | topics: { 52 | emoji: i.entity({ 53 | color: i.string().optional(), 54 | text: i.string(), 55 | }), 56 | }, 57 | }, 58 | }, 59 | }); 60 | 61 | // This helps Typescript display nicer intellisense 62 | type _AppSchema = typeof _schema; 63 | interface AppSchema extends _AppSchema {} 64 | const schema: AppSchema = _schema; 65 | 66 | export type { AppSchema }; 67 | export default schema; 68 | -------------------------------------------------------------------------------- /sandbox/vite-vue/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vite-vue", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite --host", 8 | "build": "vue-tsc -b && vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "@dorilama/instantdb-vue": "file:../..", 13 | "@vueuse/core": "^11.0.3", 14 | "vue": "^3.4.37", 15 | "vue-router": "^4.4.3" 16 | }, 17 | "devDependencies": { 18 | "@iconify-json/mdi": "^1.2.0", 19 | "@iconify/tailwind": "^1.1.3", 20 | "@types/node": "^22.5.3", 21 | "@vitejs/plugin-vue": "^5.1.2", 22 | "autoprefixer": "^10.4.20", 23 | "daisyui": "^4.12.10", 24 | "postcss": "^8.4.45", 25 | "tailwindcss": "^3.4.10", 26 | "typescript": "^5.5.3", 27 | "vite": "^6.0.3", 28 | "vue-tsc": "^2.0.29" 29 | }, 30 | "overrides": { 31 | "@jridgewell/gen-mapping": "0.3.5" 32 | } 33 | } -------------------------------------------------------------------------------- /sandbox/vite-vue/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /sandbox/vite-vue/src/App.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /sandbox/vite-vue/src/components/BottomNav.vue: -------------------------------------------------------------------------------- 1 | 76 | 88 | -------------------------------------------------------------------------------- /sandbox/vite-vue/src/components/ErrorBoundary.vue: -------------------------------------------------------------------------------- 1 | 7 | 15 | -------------------------------------------------------------------------------- /sandbox/vite-vue/src/components/Header.vue: -------------------------------------------------------------------------------- 1 | 48 | 58 | -------------------------------------------------------------------------------- /sandbox/vite-vue/src/components/ThemeController.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 36 | -------------------------------------------------------------------------------- /sandbox/vite-vue/src/components/TodoFooter.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /sandbox/vite-vue/src/components/TodoForm.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /sandbox/vite-vue/src/components/TodoList.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /sandbox/vite-vue/src/components/UserSample.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /sandbox/vite-vue/src/db/composables.ts: -------------------------------------------------------------------------------- 1 | import { computed } from "vue"; 2 | import { useRoute, useRouter } from "vue-router"; 3 | import { db, chatRoom } from "@/db"; 4 | 5 | export const fixedRandomColor = 6 | "#" + Math.floor(Math.random() * 16777215).toString(16); 7 | 8 | export const anonUser = `Anon-${Math.random().toString(36).slice(2, 6)}`; 9 | 10 | export function useUserPresenceValue() { 11 | const route = useRoute(); 12 | 13 | const { user } = db.useAuth(); 14 | const userPresence = computed(() => { 15 | const userId = 16 | user.value?.id && !route.query.anon ? user.value?.id : anonUser; 17 | return { 18 | userId, 19 | color: fixedRandomColor, 20 | path: route.path, 21 | }; 22 | }); 23 | 24 | return userPresence; 25 | } 26 | 27 | export function usePeerStats() { 28 | const router = useRouter(); 29 | const routes = router.getRoutes().filter((r) => r.meta.isNav === true); 30 | const home = routes.find((r) => r.path == "/") || { 31 | path: "/", 32 | meta: {} as Record, 33 | }; 34 | const { peers, user } = db.rooms.usePresence(chatRoom); 35 | 36 | const count = computed<{ 37 | byPath: Record<(typeof routes)[number]["path"], number>; 38 | notInHome: number; 39 | total: number; 40 | }>(() => { 41 | return Object.values(peers.value).reduce( 42 | (count, peer) => { 43 | if (routes.find((r) => r.meta.isNav && r.path === peer.path)) { 44 | count.byPath[peer.path] = (count.byPath[peer.path] || 0) + 1; 45 | if (peer.path !== home.path) { 46 | count.notInHome += 1; 47 | } 48 | count.total += 1; 49 | } 50 | 51 | return count; 52 | }, 53 | { 54 | byPath: {} as Record<(typeof routes)[number]["path"], number>, 55 | notInHome: 0, 56 | total: 0, 57 | } 58 | ); 59 | }); 60 | return { user, peers, home, routes, count }; 61 | } 62 | -------------------------------------------------------------------------------- /sandbox/vite-vue/src/db/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | init, 3 | i, 4 | type InstaQLParams, 5 | InstaQLResult, 6 | } from "@dorilama/instantdb-vue"; 7 | 8 | import schema from "../../instant.schema"; 9 | 10 | // Visit https://instantdb.com/dash to get your APP_ID :) 11 | const APP_ID = import.meta.env["VITE_INSTANT_APP_ID"]; 12 | 13 | export const db = init({ appId: APP_ID, schema, useDateObjects: true }); 14 | export const chatRoom = db.room("chat", "dev"); 15 | 16 | const todosQuery = { todos: {} } satisfies InstaQLParams; 17 | 18 | type TodosResult = InstaQLResult; 19 | 20 | export type Todo = TodosResult["todos"][number]; 21 | -------------------------------------------------------------------------------- /sandbox/vite-vue/src/db/todo.ts: -------------------------------------------------------------------------------- 1 | import { id } from "@dorilama/instantdb-vue"; 2 | import type { Todo } from "@/db"; 3 | import { db } from "@/db"; 4 | 5 | export function addTodo(text: string) { 6 | if (!text) { 7 | return; 8 | } 9 | db.transact( 10 | db.tx.todos[id()].update({ 11 | text, 12 | done: false, 13 | createdAt: Date.now(), 14 | lastModified: Date.now(), 15 | }) 16 | ); 17 | } 18 | 19 | export function toggleAll(todos: Todo[] = []) { 20 | const newVal = todos.some((todo) => !todo.done); 21 | db.transact( 22 | todos.map((todo) => 23 | db.tx.todos[todo.id].update({ done: newVal, lastModified: Date.now() }) 24 | ) 25 | ); 26 | } 27 | 28 | export function willCheckAll(todos: Todo[] = []) { 29 | return todos.some((todo) => !todo.done); 30 | } 31 | 32 | export function deleteCompleted(todos: Todo[]) { 33 | const completed = todos.filter((todo) => todo.done); 34 | const txs = completed.map((todo) => db.tx.todos[todo.id].delete()); 35 | db.transact(txs); 36 | } 37 | 38 | export function toggleDone(todo: Todo) { 39 | db.transact( 40 | db.tx.todos[todo.id].update({ done: !todo.done, lastModified: Date.now() }) 41 | ); 42 | } 43 | 44 | export function deleteTodo(todo: Todo) { 45 | db.transact(db.tx.todos[todo.id].delete()); 46 | } 47 | -------------------------------------------------------------------------------- /sandbox/vite-vue/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --dvh-full: 100dvh; 7 | } 8 | 9 | #app { 10 | min-height: var(--dvh-full, 100vh); 11 | } 12 | -------------------------------------------------------------------------------- /sandbox/vite-vue/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from "vue"; 2 | import App from "./App.vue"; 3 | import { router } from "@/router"; 4 | import "@/index.css"; 5 | 6 | createApp(App).use(router).mount("#app"); 7 | -------------------------------------------------------------------------------- /sandbox/vite-vue/src/router.ts: -------------------------------------------------------------------------------- 1 | import { createWebHistory, createRouter } from "vue-router"; 2 | 3 | const routes = [ 4 | { 5 | path: "/", 6 | name: "home", 7 | component: () => import("@/views/Todo.vue"), 8 | meta: { label: "Todo", isNav: true }, 9 | }, 10 | { 11 | path: "/signin", 12 | name: "signin", 13 | component: () => import("@/views/Signin.vue"), 14 | meta: { label: "Sign in" }, 15 | }, 16 | { path: "/signup", redirect: "/signin" }, 17 | { 18 | path: "/cursors", 19 | name: "cursors", 20 | component: () => import("@/views/Cursors.vue"), 21 | meta: { label: "Cursors", isNav: true }, 22 | }, 23 | { 24 | path: "/cursors-iframe", 25 | name: "cursorsIframe", 26 | component: () => import("@/views/CursorsIframe.vue"), 27 | }, 28 | { 29 | path: "/typing", 30 | name: "typing", 31 | component: () => import("@/views/Typing.vue"), 32 | meta: { label: "Typing", isNav: true }, 33 | }, 34 | { 35 | path: "/typing-iframe", 36 | name: "typingIframe", 37 | component: () => import("@/views/TypingIframe.vue"), 38 | }, 39 | { 40 | path: "/topics", 41 | name: "topics", 42 | component: () => import("@/views/Topics.vue"), 43 | meta: { label: "Topics", isNav: true }, 44 | }, 45 | { 46 | path: "/topics-iframe", 47 | name: "topicsIframe", 48 | component: () => import("@/views/TopicsIframe.vue"), 49 | }, 50 | { 51 | path: "/rooms", 52 | name: "rooms", 53 | component: () => import("@/views/Rooms.vue"), 54 | meta: { label: "Rooms", isNav: true }, 55 | }, 56 | { 57 | path: "/docs", 58 | name: "docs", 59 | component: () => import("@/views/Docs.vue"), 60 | meta: { label: "Docs", isNav: true }, 61 | }, 62 | ]; 63 | 64 | export const router = createRouter({ 65 | history: createWebHistory(), 66 | routes, 67 | scrollBehavior(to, from, savedPosition) { 68 | if (savedPosition) { 69 | return savedPosition; 70 | } else if (to.hash) { 71 | return { 72 | el: to.hash, 73 | }; 74 | } else { 75 | return { top: 0 }; 76 | } 77 | }, 78 | linkActiveClass: "active", 79 | }); 80 | -------------------------------------------------------------------------------- /sandbox/vite-vue/src/utils/composables.ts: -------------------------------------------------------------------------------- 1 | import { onMounted, watchEffect } from "vue"; 2 | import { useStorage, useMounted } from "@vueuse/core"; 3 | 4 | export function useHideInstantDevTools() { 5 | onMounted(() => { 6 | const el = document.body.lastElementChild as HTMLElement; 7 | const app = document.getElementById("app"); 8 | if (app && el && !app.contains(el)) { 9 | el.style.display = "none"; 10 | } 11 | }); 12 | } 13 | 14 | export function useLocalSettings() { 15 | return useStorage( 16 | "settings", 17 | { 18 | theme: globalThis.matchMedia?.("(prefers-color-scheme: dark)").matches 19 | ? "dark" 20 | : "ligh", 21 | }, 22 | localStorage, 23 | { mergeDefaults: true } 24 | ); 25 | } 26 | 27 | export function useThemeUpdater() { 28 | const settings = useLocalSettings(); 29 | const isMounted = useMounted(); 30 | 31 | watchEffect(() => { 32 | if (!isMounted) { 33 | return; 34 | } 35 | document.documentElement.dataset.theme = settings.value.theme; 36 | }); 37 | } 38 | -------------------------------------------------------------------------------- /sandbox/vite-vue/src/views/Cursors.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 54 | 55 | 61 | -------------------------------------------------------------------------------- /sandbox/vite-vue/src/views/CursorsIframe.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 44 | -------------------------------------------------------------------------------- /sandbox/vite-vue/src/views/Docs.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /sandbox/vite-vue/src/views/Rooms.vue: -------------------------------------------------------------------------------- 1 | 51 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /sandbox/vite-vue/src/views/Signin.vue: -------------------------------------------------------------------------------- 1 | 58 | 59 | 122 | -------------------------------------------------------------------------------- /sandbox/vite-vue/src/views/Todo.vue: -------------------------------------------------------------------------------- 1 | 89 | 90 | 209 | 210 | 211 | -------------------------------------------------------------------------------- /sandbox/vite-vue/src/views/Topics.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 38 | -------------------------------------------------------------------------------- /sandbox/vite-vue/src/views/TopicsIframe.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 73 | -------------------------------------------------------------------------------- /sandbox/vite-vue/src/views/Typing.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 42 | -------------------------------------------------------------------------------- /sandbox/vite-vue/src/views/TypingIframe.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 67 | -------------------------------------------------------------------------------- /sandbox/vite-vue/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /sandbox/vite-vue/tailwind.config.js: -------------------------------------------------------------------------------- 1 | import daisyui from "daisyui"; 2 | import { addDynamicIconSelectors } from "@iconify/tailwind"; 3 | /** @type {import('tailwindcss').Config} */ 4 | export default { 5 | content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx,vue}"], 6 | theme: { 7 | extend: { 8 | keyframes: { 9 | tada: { 10 | "0%": { 11 | transform: "scale3d(1, 1, 1)", 12 | }, 13 | "10%, 20%": { 14 | transform: "scale3d(0.9, 0.9, 0.9) rotate3d(0, 0, 1, -3deg)", 15 | }, 16 | "30%, 50%, 70%, 90%": { 17 | transform: "scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg)", 18 | }, 19 | "40%, 60%, 80%": { 20 | transform: "scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg)", 21 | }, 22 | "100%": { 23 | transform: "scale3d(1, 1, 1)", 24 | }, 25 | }, 26 | }, 27 | animation: { tada: "tada 1s ease-in-out 0.25s 1" }, 28 | }, 29 | }, 30 | plugins: [daisyui, addDynamicIconSelectors()], 31 | daisyui: { 32 | themes: ["light", "dark"], 33 | }, 34 | darkMode: ["class", '[data-theme="dark"]'], 35 | }; 36 | -------------------------------------------------------------------------------- /sandbox/vite-vue/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.paths", 3 | "compilerOptions": { 4 | "target": "ES2020", 5 | "useDefineForClassFields": true, 6 | "module": "ESNext", 7 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 8 | "skipLibCheck": true, 9 | 10 | /* Bundler mode */ 11 | "moduleResolution": "bundler", 12 | "allowImportingTsExtensions": true, 13 | "isolatedModules": true, 14 | "moduleDetection": "force", 15 | "noEmit": true, 16 | "jsx": "preserve", 17 | 18 | /* Linting */ 19 | "strict": true, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | "noFallthroughCasesInSwitch": true 23 | }, 24 | "include": [ 25 | "src/**/*.ts", 26 | "src/**/*.tsx", 27 | "src/**/*.vue", 28 | "istant.perms.ts", 29 | "instant.schema.ts" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /sandbox/vite-vue/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./tsconfig.app.json" }, 5 | { "path": "./tsconfig.node.json" } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /sandbox/vite-vue/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.paths", 3 | "compilerOptions": { 4 | "target": "ES2022", 5 | "lib": ["ES2023"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "isolatedModules": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noFallthroughCasesInSwitch": true 21 | }, 22 | "include": ["vite.config.ts"] 23 | } 24 | -------------------------------------------------------------------------------- /sandbox/vite-vue/tsconfig.paths.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "paths": { 4 | "@dorilama/instantdb-vue": ["../../src"], 5 | "@/*": ["./src/*"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /sandbox/vite-vue/vite.config.ts: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import { defineConfig } from "vite"; 3 | import vue from "@vitejs/plugin-vue"; 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [vue()], 8 | resolve: { 9 | alias: { 10 | "@dorilama/instantdb-vue": path.resolve(__dirname, "../../src"), 11 | "@": path.resolve(__dirname, "./src"), 12 | }, 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /scripts/prepublish.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import fs from "node:fs/promises"; 3 | import pkg from "../package.json" with {type:'json'}; 4 | 5 | const url = new URL("../src/version.ts", import.meta.url); 6 | await fs.writeFile( 7 | url, 8 | `// Autogenerated by prepublish.js 9 | const version = "v${pkg.version}"; 10 | export default version;` 11 | ); 12 | -------------------------------------------------------------------------------- /src/InstantVueAbstractDatabase.ts: -------------------------------------------------------------------------------- 1 | // Notice: 2 | // adapted from [@instantdb/react](https://github.com/instantdb/instant/blob/main/client/packages/react/README.md) 3 | // see instantdb-license.md for license 4 | 5 | import { 6 | Auth, 7 | Storage, 8 | txInit, 9 | InstantCoreDatabase, 10 | init as core_init, 11 | InstantError, 12 | } from "@instantdb/core"; 13 | import type { 14 | AuthState, 15 | User, 16 | ConnectionStatus, 17 | TransactionChunk, 18 | RoomSchemaShape, 19 | InstaQLOptions, 20 | PageInfoResponse, 21 | InstaQLResponse, 22 | RoomsOf, 23 | InstantSchemaDef, 24 | IInstantDatabase, 25 | ValidQuery, 26 | } from "@instantdb/core"; 27 | import { useQueryInternal } from "./useQuery"; 28 | import type { UseQueryInternalReturn } from "./useQuery"; 29 | import { 30 | computed, 31 | onMounted, 32 | ref, 33 | shallowRef, 34 | toValue, 35 | watchEffect, 36 | h, 37 | defineComponent, 38 | type SetupContext, 39 | type SlotsType, 40 | } from "vue"; 41 | import type { MaybeRefOrGetter, Ref, ShallowRef } from "vue"; 42 | import { tryOnScopeDispose } from "./utils"; 43 | import type { InstantConfig, Extra } from "./init"; 44 | import { InstantVueRoom, rooms } from "./InstantVueRoom"; 45 | 46 | type UseAuthReturn = { [K in keyof AuthState]: ShallowRef }; 47 | 48 | export default abstract class InstantVueAbstractDatabase< 49 | Schema extends InstantSchemaDef, 50 | UseDates extends boolean = false, 51 | Config extends InstantConfig = InstantConfig< 52 | Schema, 53 | UseDates 54 | >, 55 | Rooms extends RoomSchemaShape = RoomsOf 56 | > implements IInstantDatabase 57 | { 58 | public tx = txInit(); 59 | 60 | public auth: Auth; 61 | public storage: Storage; 62 | public core: InstantCoreDatabase; 63 | 64 | /** @deprecated use `core` instead */ 65 | public _core: InstantCoreDatabase; 66 | 67 | static Storage?: any; 68 | static NetworkListener?: any; 69 | static EventSourceImpl?: any; 70 | 71 | static extra: Extra; 72 | 73 | constructor( 74 | config: Omit, "useDateObjects"> & { 75 | useDateObjects?: UseDates; 76 | }, 77 | versions?: { [key: string]: string } 78 | ) { 79 | const { __extra_vue, ..._config } = config; 80 | 81 | if (_config.clientOnlyUseQuery) { 82 | console.warn( 83 | `clientOnlyUseQuery is deprecated. use __extra_vue.clientOnlyUseQuery` 84 | ); 85 | } 86 | 87 | this.core = core_init( 88 | _config, 89 | // @ts-expect-error because TS can't resolve subclass statics 90 | this.constructor.Storage, 91 | // @ts-expect-error because TS can't resolve subclass statics 92 | this.constructor.NetworkListener, 93 | versions, 94 | // @ts-expect-error because TS can't resolve subclass static 95 | this.constructor.EventSourceImpl 96 | ); 97 | this._core = this.core; 98 | this.auth = this.core.auth; 99 | this.storage = this.core.storage; 100 | // @ts-expect-error because TS can't resolve subclass statics 101 | this.constructor.extra = { 102 | clientOnlyUseQuery: 103 | !!__extra_vue?.clientOnlyUseQuery || !!_config.clientOnlyUseQuery, 104 | stopLoadingOnNullQuery: !!__extra_vue?.stopLoadingOnNullQuery, 105 | } satisfies Extra; 106 | } 107 | 108 | /** 109 | * Returns a unique ID for a given `name`. It's stored in local storage, 110 | * so you will get the same ID across sessions. 111 | * 112 | * This is useful for generating IDs that could identify a local device or user. 113 | * 114 | * @example 115 | * const deviceId = await db.getLocalId('device'); 116 | */ 117 | getLocalId = (name: string) => { 118 | return this.core.getLocalId(name); 119 | }; 120 | 121 | /** 122 | * A hook that returns a unique ID for a given `name`. localIds are 123 | * stored in local storage, so you will get the same ID across sessions. 124 | * 125 | * Initially returns `null`, and then loads the localId. 126 | * 127 | * @example 128 | * const deviceId = db.useLocalId('device'); 129 | * watch(deviceId, (value)=>{ 130 | * if(value){ 131 | * console.log('Device ID:', value) 132 | * } 133 | * }) 134 | */ 135 | useLocalId = (name: MaybeRefOrGetter): Ref => { 136 | const localId = ref(null); 137 | 138 | const isMounted = ref(false); 139 | 140 | onMounted(() => { 141 | isMounted.value = true; 142 | }); 143 | 144 | watchEffect(async () => { 145 | const _name = toValue(name); 146 | if (!isMounted.value) { 147 | return; 148 | } 149 | const id = await this.getLocalId(_name); 150 | if (toValue(name) === _name) { 151 | localId.value = id; 152 | } 153 | }); 154 | 155 | return localId; 156 | }; 157 | 158 | /** 159 | * Obtain a handle to a room, which allows you to listen to topics and presence data 160 | * 161 | * If you don't provide a `type` or `id`, Instant will default to `_defaultRoomType` and `_defaultRoomId` 162 | * as the room type and id, respectively. 163 | * 164 | * @see https://instantdb.com/docs/presence-and-topics 165 | * 166 | * @example 167 | * const room = db.room('chat', roomId); 168 | * const { peers } = db.rooms.usePresence(room); 169 | */ 170 | room( 171 | type?: MaybeRefOrGetter, 172 | id?: MaybeRefOrGetter 173 | ) { 174 | const _type = computed(() => { 175 | return toValue(type) || ("_defaultRoomType" as RoomType); 176 | }); 177 | const _id = computed(() => { 178 | return toValue(id) || "_defaultRoomId"; 179 | }); 180 | return new InstantVueRoom(this.core, _type, _id); 181 | } 182 | 183 | /** 184 | * Hooks for working with rooms 185 | * 186 | * @see https://instantdb.com/docs/presence-and-topics 187 | * 188 | * @example 189 | * const room = db.room('chat', roomId); 190 | * const { peers } = db.rooms.usePresence(room); 191 | * const publish = db.rooms.usePublishTopic(room, 'emoji'); 192 | * // ... 193 | */ 194 | rooms = rooms; 195 | 196 | /** 197 | * Use this to write data! You can create, update, delete, and link objects 198 | * 199 | * @see https://instantdb.com/docs/instaml 200 | * 201 | * @example 202 | * // Create a new object in the `goals` namespace 203 | * const goalId = id(); 204 | * db.transact(db.tx.goals[goalId].update({title: "Get fit"})) 205 | * 206 | * // Update the title 207 | * db.transact(db.tx.goals[goalId].update({title: "Get super fit"})) 208 | * 209 | * // Delete it 210 | * db.transact(db.tx.goals[goalId].delete()) 211 | * 212 | * // Or create an association: 213 | * todoId = id(); 214 | * db.transact([ 215 | * db.tx.todos[todoId].update({ title: 'Go on a run' }), 216 | * db.tx.goals[goalId].link({todos: todoId}), 217 | * ]) 218 | */ 219 | transact = ( 220 | chunks: TransactionChunk | TransactionChunk[] 221 | ) => { 222 | return this.core.transact(chunks); 223 | }; 224 | 225 | /** 226 | * Use this to query your data! 227 | * 228 | * @see https://instantdb.com/docs/instaql 229 | * 230 | * @example 231 | * // listen to all goals 232 | * const { isLoading, error, data } = db.useQuery({ goals: {} }) 233 | * 234 | * // goals where the title is "Get Fit" 235 | * const { isLoading, error, data } = db.useQuery({ 236 | * goals: { $: { where: { title: "Get Fit" } } } 237 | * }) 238 | * 239 | * // all goals, _alongside_ their todos 240 | * const { isLoading, error, data } = db.useQuery({ 241 | * goals: { todos: {} } 242 | * }) 243 | * 244 | * // skip if `user` is not logged in 245 | * const { isLoading, error, data } = db.useQuery( 246 | * auth.user ? { goals: {} } : null 247 | * ) 248 | */ 249 | useQuery = >( 250 | query: MaybeRefOrGetter, 251 | opts?: MaybeRefOrGetter 252 | ): UseQueryInternalReturn => { 253 | return useQueryInternal( 254 | this.core, 255 | query, 256 | opts, 257 | // @ts-expect-error because TS can't resolve subclass statics 258 | this.constructor.extra 259 | ).state; 260 | }; 261 | 262 | /** 263 | * Subscribe to the currently logged in user. 264 | * If the user is not logged in, this hook with throw an Error. 265 | * You will want to protect any calls of this hook with a 266 | * component, or your own logic based on db.useAuth() 267 | * 268 | * @see https://instantdb.com/docs/auth 269 | * @throws Error indicating user not signed in 270 | * @example 271 | * function UserDisplay() { 272 | * const user = db.useUser() 273 | * return
Logged in as: {user.email}
274 | * } 275 | * 276 | * 277 | * 278 | * 279 | * 280 | */ 281 | 282 | useUser = (): ShallowRef => { 283 | const { user } = this.useAuth(); 284 | 285 | if (!user.value) { 286 | throw new InstantError( 287 | "useUser must be used within an auth-protected route" 288 | ); 289 | } 290 | 291 | return user as ShallowRef; 292 | }; 293 | 294 | /** 295 | * Listen for the logged in state. This is useful 296 | * for deciding when to show a login screen. 297 | * 298 | * Check out the docs for an example `Login` component too! 299 | * 300 | * @see https://instantdb.com/docs/auth 301 | * @example 302 | * 305 | * 311 | */ 312 | useAuth = (): UseAuthReturn & { stop: () => void } => { 313 | const initialState = this.core._reactor._currentUserCached; 314 | 315 | const state: UseAuthReturn & { stop: () => void } = { 316 | isLoading: ref(initialState.isLoading), 317 | user: shallowRef(initialState.user), 318 | error: shallowRef(initialState.error), 319 | stop: () => {}, 320 | }; 321 | const unsubscribe = this.core._reactor.subscribeAuth((resp: any) => { 322 | state.isLoading.value = false; 323 | state.user.value = resp.user; 324 | state.error.value = resp.error; 325 | }); 326 | 327 | state.stop = () => { 328 | unsubscribe(); 329 | }; 330 | 331 | tryOnScopeDispose(() => { 332 | unsubscribe(); 333 | }); 334 | 335 | return state; 336 | }; 337 | 338 | /** 339 | * One time query for the logged in state. This is useful 340 | * for scenarios where you want to know the current auth 341 | * state without subscribing to changes. 342 | * 343 | * @see https://instantdb.com/docs/auth 344 | * @example 345 | * const user = await db.getAuth(); 346 | * console.log('logged in as', user.email) 347 | */ 348 | getAuth = (): Promise => { 349 | return this.core.getAuth(); 350 | }; 351 | 352 | /** 353 | * Listen for connection status changes to Instant. Use this for things like 354 | * showing connection state to users 355 | * 356 | * @see https://www.instantdb.com/docs/patterns#connection-status 357 | * @example 358 | * 370 | * 373 | */ 374 | useConnectionStatus = (): Ref => { 375 | const status = ref( 376 | this.core._reactor.status as ConnectionStatus 377 | ); 378 | const unsubscribe = this.core.subscribeConnectionStatus((newStatus) => { 379 | status.value = newStatus; 380 | }); 381 | 382 | tryOnScopeDispose(unsubscribe); 383 | 384 | return status; 385 | }; 386 | 387 | /** 388 | * Use this for one-off queries. 389 | * Returns local data if available, otherwise fetches from the server. 390 | * Because we want to avoid stale data, this method will throw an error 391 | * if the user is offline or there is no active connection to the server. 392 | * 393 | * @see https://instantdb.com/docs/instaql 394 | * 395 | * @example 396 | * 397 | * const resp = await db.queryOnce({ goals: {} }); 398 | * console.log(resp.data.goals) 399 | */ 400 | queryOnce = >( 401 | query: Q, 402 | opts?: InstaQLOptions 403 | ): Promise<{ 404 | data: InstaQLResponse; 405 | pageInfo: PageInfoResponse; 406 | }> => { 407 | return this.core.queryOnce(query, opts); 408 | }; 409 | 410 | /** 411 | * Only render children if the user is signed in. 412 | * @see https://instantdb.com/docs/auth 413 | * 414 | * @example 415 | * 416 | * 417 | * 418 | */ 419 | SignedIn = componentWithDb(this, (db) => 420 | defineComponent({ 421 | setup: (_, { slots }) => { 422 | const auth = db.useAuth(); 423 | return () => { 424 | if (auth.isLoading.value || auth.error.value || !auth.user.value) 425 | return null; 426 | return slots.default?.(); 427 | }; 428 | }, 429 | }) 430 | ); 431 | 432 | /** 433 | * Only render children if the user is signed out. 434 | * @see https://instantdb.com/docs/auth 435 | * 436 | * @example 437 | * 438 | * 439 | * 440 | */ 441 | SignedOut = componentWithDb(this, (db) => 442 | defineComponent({ 443 | setup(_, { slots }) { 444 | const auth = db.useAuth(); 445 | return () => { 446 | if (auth.isLoading.value || auth.error.value || auth.user.value) 447 | return null; 448 | return slots.default?.(); 449 | }; 450 | }, 451 | }) 452 | ); 453 | } 454 | 455 | function componentWithDb< 456 | Schema extends InstantSchemaDef, 457 | UseDates extends boolean = false, 458 | Config extends InstantConfig = InstantConfig< 459 | Schema, 460 | UseDates 461 | >, 462 | Rooms extends RoomSchemaShape = RoomsOf 463 | >( 464 | db: InstantVueAbstractDatabase, 465 | defineComponentCallback: ( 466 | db: InstantVueAbstractDatabase 467 | ) => ReturnType 468 | ) { 469 | return defineComponentCallback(db); 470 | } 471 | -------------------------------------------------------------------------------- /src/InstantVueRoom.ts: -------------------------------------------------------------------------------- 1 | // Notice: 2 | // adapted from [@instantdb/react](https://github.com/instantdb/instant/blob/main/client/packages/react/README.md) 3 | // see instantdb-license.md for license 4 | 5 | import type { 6 | PresenceOpts, 7 | PresenceResponse, 8 | RoomSchemaShape, 9 | InstantCoreDatabase, 10 | InstantSchemaDef, 11 | } from "@instantdb/core"; 12 | 13 | import { computed, ref, shallowRef, toValue, watchEffect } from "vue"; 14 | import type { ComputedRef, MaybeRefOrGetter, Ref, ShallowRef } from "vue"; 15 | 16 | import { useTimeout } from "./useTimeout"; 17 | import { tryOnScopeDispose, type Arrayable } from "./utils"; 18 | 19 | export type PresenceHandle< 20 | PresenceShape, 21 | Keys extends keyof PresenceShape, 22 | State = PresenceResponse 23 | > = { [K in keyof State]: ShallowRef } & { 24 | publishPresence: (data?: Partial) => void; 25 | stop: () => void; 26 | }; 27 | 28 | export type TypingIndicatorOpts = { 29 | timeout?: number | null; 30 | stopOnEnter?: boolean; 31 | // Perf opt - `active` will always be an empty array 32 | writeOnly?: boolean; 33 | }; 34 | 35 | export type TypingIndicatorHandle = { 36 | active: Ref; 37 | setActive(active: boolean): void; 38 | inputProps: { 39 | onKeyDown: (e: KeyboardEvent) => void; 40 | onBlur: () => void; 41 | }; 42 | stop: () => void; 43 | }; 44 | 45 | export const defaultActivityStopTimeout = 1_000; 46 | 47 | // ------ 48 | // #region Topics 49 | 50 | /** 51 | * Listen for broadcasted events given a room and topic. 52 | * 53 | * @see https://instantdb.com/docs/presence-and-topics 54 | * @example 55 | * 64 | */ 65 | export function useTopicEffect< 66 | RoomSchema extends RoomSchemaShape, 67 | RoomType extends keyof RoomSchema, 68 | TopicType extends keyof RoomSchema[RoomType]["topics"] 69 | >( 70 | room: InstantVueRoom, 71 | topic: MaybeRefOrGetter>, 72 | onEvent: Arrayable< 73 | ( 74 | event: RoomSchema[RoomType]["topics"][TopicType], 75 | peer: RoomSchema[RoomType]["presence"], 76 | topic: TopicType 77 | ) => any 78 | > 79 | ): () => void { 80 | const cleanup: (() => void)[] = []; 81 | 82 | function unsubscribe() { 83 | cleanup.forEach((fn) => fn()); 84 | cleanup.length = 0; 85 | } 86 | 87 | const stop = watchEffect((onCleanup) => { 88 | const _topic = toValue(topic); 89 | const id = room.id.value; 90 | const topicArray = Array.isArray(_topic) ? _topic : [_topic]; 91 | const callbacks = Array.isArray(onEvent) ? onEvent : [onEvent]; 92 | cleanup.push( 93 | ...topicArray.map((topicType) => { 94 | return room.core._reactor.subscribeTopic( 95 | id, 96 | topicType, 97 | ( 98 | event: RoomSchema[RoomType]["topics"][TopicType], 99 | peer: RoomSchema[RoomType]["presence"] 100 | ) => { 101 | callbacks.forEach((cb) => { 102 | cb(event, peer, topicType); 103 | }); 104 | } 105 | ); 106 | }) 107 | ); 108 | onCleanup(unsubscribe); 109 | }); 110 | 111 | tryOnScopeDispose(() => { 112 | stop(); 113 | }); 114 | 115 | return stop; 116 | } 117 | 118 | /** 119 | * Broadcast an event to a room. 120 | * 121 | * @see https://instantdb.com/docs/presence-and-topics 122 | * @example 123 | * 128 | * 131 | */ 132 | export function usePublishTopic< 133 | RoomSchema extends RoomSchemaShape, 134 | RoomType extends keyof RoomSchema, 135 | TopicType extends keyof RoomSchema[RoomType]["topics"] 136 | >( 137 | room: InstantVueRoom, 138 | topic: MaybeRefOrGetter 139 | ): (data: RoomSchema[RoomType]["topics"][TopicType]) => void { 140 | const stopRoomWatch = watchEffect((onCleanup) => { 141 | const id = room.id.value; 142 | const cleanup = room.core._reactor.joinRoom(id); 143 | onCleanup(cleanup); 144 | }); 145 | 146 | let publishTopic = (data: RoomSchema[RoomType]["topics"][TopicType]) => {}; 147 | 148 | const stopTopicWatch = watchEffect(() => { 149 | const id = room.id.value; 150 | const type = room.type.value; 151 | const _topic = toValue(topic); 152 | publishTopic = (data: RoomSchema[RoomType]["topics"][TopicType]) => { 153 | room.core._reactor.publishTopic({ 154 | roomType: type, 155 | roomId: id, 156 | topic: _topic, 157 | data, 158 | }); 159 | }; 160 | }); 161 | 162 | tryOnScopeDispose(() => { 163 | stopRoomWatch(); 164 | stopTopicWatch(); 165 | }); 166 | 167 | return publishTopic; 168 | } 169 | 170 | // #endregion 171 | 172 | // --------- 173 | // #region Presence 174 | 175 | /** 176 | * Listen for peer's presence data in a room, and publish the current user's presence. 177 | * 178 | * @see https://instantdb.com/docs/presence-and-topics 179 | * @example 180 | * 190 | */ 191 | export function usePresence< 192 | RoomSchema extends RoomSchemaShape, 193 | RoomType extends keyof RoomSchema, 194 | Keys extends keyof RoomSchema[RoomType]["presence"] 195 | >( 196 | room: InstantVueRoom, 197 | opts: MaybeRefOrGetter< 198 | PresenceOpts 199 | > = {} 200 | ): PresenceHandle { 201 | const getInitialState = (): PresenceResponse< 202 | RoomSchema[RoomType]["presence"], 203 | Keys 204 | > => { 205 | const presence = room.core._reactor.getPresence( 206 | room.type.value, 207 | room.id.value, 208 | toValue(opts) 209 | ) ?? { 210 | peers: {}, 211 | isLoading: true, 212 | }; 213 | 214 | return { 215 | peers: presence.peers, 216 | isLoading: !!presence.isLoading, 217 | user: presence.isLoading ? undefined : presence.user, 218 | error: presence.isLoading ? undefined : presence.error, 219 | }; 220 | }; 221 | 222 | const state = { 223 | peers: shallowRef({}), 224 | isLoading: ref(false), 225 | user: shallowRef(undefined), 226 | error: shallowRef(undefined), 227 | }; 228 | 229 | const stop = watchEffect((onCleanup) => { 230 | const id = room.id.value; 231 | const type = room.type.value; 232 | const _opts = toValue(opts); 233 | 234 | Object.entries(getInitialState()).forEach(([key, value]) => { 235 | state[ 236 | key as keyof PresenceResponse 237 | ].value = value; 238 | }); 239 | 240 | // @instantdb/core v0.14.30 removes types for subscribePresence 241 | // trying to restore types until fixed in core 242 | // by adding type to parameter in callback 243 | const unsubscribe = room.core._reactor.subscribePresence( 244 | type, 245 | id, 246 | _opts, 247 | (data: PresenceResponse) => { 248 | Object.entries(data).forEach(([key, value]) => { 249 | state[ 250 | key as keyof PresenceResponse< 251 | RoomSchema[RoomType]["presence"], 252 | Keys 253 | > 254 | ].value = value; 255 | }); 256 | } 257 | ); 258 | onCleanup(unsubscribe); 259 | }); 260 | 261 | tryOnScopeDispose(() => { 262 | stop(); 263 | }); 264 | 265 | return { 266 | ...state, 267 | publishPresence: (data) => { 268 | room.core._reactor.publishPresence(room.type.value, room.id.value, data); 269 | }, 270 | stop, 271 | }; 272 | } 273 | 274 | /** 275 | * Publishes presence data to a room 276 | * 277 | * @see https://instantdb.com/docs/presence-and-topics 278 | * @example 279 | * 286 | */ 287 | export function useSyncPresence< 288 | RoomSchema extends RoomSchemaShape, 289 | RoomType extends keyof RoomSchema 290 | >( 291 | room: InstantVueRoom, 292 | data: MaybeRefOrGetter>, 293 | deps?: MaybeRefOrGetter 294 | ): () => void { 295 | const stopJoinRoom = watchEffect((onCleanup) => { 296 | const id = room.id.value; 297 | const _data = toValue(data); 298 | const cleanup = room.core._reactor.joinRoom(id, _data); 299 | onCleanup(cleanup); 300 | }); 301 | 302 | const stopPublishPresence = watchEffect(() => { 303 | const id = room.id.value; 304 | const type = room.type.value; 305 | const _data = toValue(data); 306 | toValue(deps); 307 | room.core._reactor.publishPresence(type, id, _data); 308 | }); 309 | 310 | function stop() { 311 | stopJoinRoom(); 312 | stopPublishPresence(); 313 | } 314 | 315 | tryOnScopeDispose(() => { 316 | stop(); 317 | }); 318 | 319 | return stop; 320 | } 321 | 322 | // #endregion 323 | 324 | // ----------------- 325 | // #region Typing Indicator 326 | 327 | /** 328 | * Manage typing indicator state 329 | * 330 | * @see https://instantdb.com/docs/presence-and-topics 331 | * @example 332 | * 341 | * 344 | */ 345 | export function useTypingIndicator< 346 | RoomSchema extends RoomSchemaShape, 347 | RoomType extends keyof RoomSchema 348 | >( 349 | room: InstantVueRoom, 350 | inputName: MaybeRefOrGetter, 351 | opts: MaybeRefOrGetter = {} 352 | ): TypingIndicatorHandle { 353 | const timeout = useTimeout(); 354 | 355 | const _inputName = toValue(inputName); 356 | 357 | const observedPresence = rooms.usePresence(room, () => ({ 358 | keys: [toValue(inputName)] as (keyof RoomSchema[RoomType]["presence"])[], 359 | })); 360 | 361 | const active = computed(() => { 362 | const presenceSnapshot = room.core._reactor.getPresence( 363 | room.type.value, 364 | room.id.value 365 | ); 366 | observedPresence.peers.value; 367 | 368 | return toValue(opts)?.writeOnly 369 | ? [] 370 | : Object.values(presenceSnapshot?.peers ?? {}).filter( 371 | //@ts-ignore TODO! same error in InstantReact 372 | (p) => p[_inputName] === true 373 | ); 374 | }); 375 | 376 | const setActive = (isActive: boolean) => { 377 | const _opts = toValue(opts); 378 | const _inputName = toValue(inputName); 379 | const id = room.id.value; 380 | const type = room.type.value; 381 | room.core._reactor.publishPresence(type, id, { 382 | [_inputName]: isActive, 383 | } as unknown as Partial); 384 | 385 | if (!isActive) return; 386 | 387 | if (_opts?.timeout === null || _opts?.timeout === 0) return; 388 | 389 | timeout.set(_opts?.timeout ?? defaultActivityStopTimeout, () => { 390 | room.core._reactor.publishPresence(type, id, { 391 | [_inputName]: null, 392 | } as Partial); 393 | }); 394 | }; 395 | 396 | const onKeyDown = (e: KeyboardEvent) => { 397 | const _opts = toValue(opts); 398 | const isEnter = _opts?.stopOnEnter && e.key === "Enter"; 399 | const isActive = !isEnter; 400 | 401 | setActive(isActive); 402 | }; 403 | 404 | function stop() { 405 | timeout.clear(); 406 | } 407 | 408 | tryOnScopeDispose(() => { 409 | stop(); 410 | }); 411 | 412 | return { 413 | active, 414 | setActive, 415 | inputProps: { 416 | onKeyDown, 417 | onBlur: () => { 418 | setActive(false); 419 | }, 420 | }, 421 | stop, 422 | }; 423 | } 424 | 425 | // #endregion 426 | 427 | // -------------- 428 | // #region Hooks 429 | export const rooms = { 430 | useTopicEffect, 431 | usePublishTopic, 432 | usePresence, 433 | useSyncPresence, 434 | useTypingIndicator, 435 | }; 436 | 437 | // #endregion 438 | 439 | // ------------ 440 | // #region Class 441 | 442 | export class InstantVueRoom< 443 | Schema extends InstantSchemaDef, 444 | RoomSchema extends RoomSchemaShape, 445 | RoomType extends keyof RoomSchema 446 | > { 447 | core: InstantCoreDatabase; 448 | type: ComputedRef; 449 | id: ComputedRef; 450 | 451 | constructor( 452 | core: InstantCoreDatabase, 453 | type: ComputedRef, 454 | id: ComputedRef 455 | ) { 456 | this.core = core; 457 | this.type = type; 458 | this.id = id; 459 | } 460 | 461 | /** 462 | * @deprecated 463 | * `db.room(...).useTopicEffect` is deprecated. You can replace it with `db.rooms.useTopicEffect`. 464 | * 465 | * @example 466 | * 467 | * // Before 468 | * const room = db.room('chat', 'room-id'); 469 | * room.useTopicEffect('emoji', (message, peer) => { }); 470 | * 471 | * // After 472 | * const room = db.room('chat', 'room-id'); 473 | * db.rooms.useTopicEffect(room, 'emoji', (message, peer) => { }); 474 | */ 475 | useTopicEffect = ( 476 | topic: MaybeRefOrGetter>, 477 | onEvent: Arrayable< 478 | ( 479 | event: RoomSchema[RoomType]["topics"][TopicType], 480 | peer: RoomSchema[RoomType]["presence"], 481 | topic: TopicType 482 | ) => any 483 | > 484 | ): (() => void) => { 485 | return rooms.useTopicEffect(this, topic, onEvent); 486 | }; 487 | 488 | /** 489 | * @deprecated 490 | * `db.room(...).usePublishTopic` is deprecated. You can replace it with `db.rooms.usePublishTopic`. 491 | * 492 | * @example 493 | * 494 | * // Before 495 | * const room = db.room('chat', 'room-id'); 496 | * const publish = room.usePublishTopic('emoji'); 497 | * 498 | * // After 499 | * const room = db.room('chat', 'room-id'); 500 | * const publish = db.rooms.usePublishTopic(room, 'emoji'); 501 | */ 502 | usePublishTopic = ( 503 | topic: MaybeRefOrGetter 504 | ): ((data: RoomSchema[RoomType]["topics"][Topic]) => void) => { 505 | return rooms.usePublishTopic(this, topic); 506 | }; 507 | 508 | /** 509 | * @deprecated 510 | * `db.room(...).usePresence` is deprecated. You can replace it with `db.rooms.usePresence`. 511 | * 512 | * @example 513 | * 514 | * // Before 515 | * const room = db.room('chat', 'room-id'); 516 | * const { peers } = room.usePresence({ keys: ["name", "avatar"] }); 517 | * 518 | * // After 519 | * const room = db.room('chat', 'room-id'); 520 | * const { peers } = db.rooms.usePresence(room, { keys: ["name", "avatar"] }); 521 | */ 522 | usePresence = ( 523 | opts: MaybeRefOrGetter< 524 | PresenceOpts 525 | > = {} 526 | ): PresenceHandle => { 527 | return rooms.usePresence(this, opts); 528 | }; 529 | 530 | /** 531 | * @deprecated 532 | * `db.room(...).useSyncPresence` is deprecated. You can replace it with `db.rooms.useSyncPresence`. 533 | * 534 | * @example 535 | * 536 | * // Before 537 | * const room = db.room('chat', 'room-id'); 538 | * room.useSyncPresence(room, { nickname }); 539 | * 540 | * // After 541 | * const room = db.room('chat', 'room-id'); 542 | * db.rooms.useSyncPresence(room, { nickname }); 543 | */ 544 | useSyncPresence = ( 545 | data: MaybeRefOrGetter< 546 | Partial 547 | >, 548 | deps?: MaybeRefOrGetter 549 | ): (() => void) => { 550 | return rooms.useSyncPresence(this, data, deps); 551 | }; 552 | 553 | /** 554 | * @deprecated 555 | * `db.room(...).useTypingIndicator` is deprecated. You can replace it with `db.rooms.useTypingIndicator`. 556 | * 557 | * @example 558 | * 559 | * // Before 560 | * const room = db.room('chat', 'room-id'); 561 | * const typing = room.useTypingIndiactor(room, 'chat-input'); 562 | * 563 | * // After 564 | * const room = db.room('chat', 'room-id'); 565 | * const typing = db.rooms.useTypingIndiactor(room, 'chat-input'); 566 | */ 567 | useTypingIndicator = ( 568 | inputName: MaybeRefOrGetter, 569 | opts: MaybeRefOrGetter = {} 570 | ): TypingIndicatorHandle => { 571 | return rooms.useTypingIndicator(this, inputName, opts); 572 | }; 573 | } 574 | 575 | // #endregion 576 | -------------------------------------------------------------------------------- /src/InstantVueWebDatabase.ts: -------------------------------------------------------------------------------- 1 | // Notice: 2 | // adapted from [@instantdb/react](https://github.com/instantdb/instant/blob/main/client/packages/react/README.md) 3 | // see instantdb-license.md for license 4 | 5 | import type { InstantSchemaDef } from "@instantdb/core"; 6 | import type { InstantConfig } from "./init"; 7 | import InstantVueAbstractDatabase from "./InstantVueAbstractDatabase"; 8 | import { EventSource } from "eventsource"; 9 | 10 | export default class InstantVueWebDatabase< 11 | Schema extends InstantSchemaDef, 12 | UseDates extends boolean = false, 13 | Config extends InstantConfig = InstantConfig< 14 | Schema, 15 | UseDates 16 | > 17 | > extends InstantVueAbstractDatabase { 18 | static EventSourceImpl = EventSource; 19 | } 20 | -------------------------------------------------------------------------------- /src/__types__/typeUtils.ts: -------------------------------------------------------------------------------- 1 | // Type testing utils from 2 | // https://github.com/type-challenges/type-challenges/blob/main/utils/index.d.ts 3 | 4 | export type Expect = T; 5 | export type ExpectTrue = T; 6 | export type ExpectFalse = T; 7 | export type IsTrue = T; 8 | export type IsFalse = T; 9 | 10 | export type Equal = (() => T extends X ? 1 : 2) extends < 11 | T 12 | >() => T extends Y ? 1 : 2 13 | ? true 14 | : false; 15 | export type NotEqual = true extends Equal ? false : true; 16 | 17 | // https://stackoverflow.com/questions/49927523/disallow-call-with-any/49928360#49928360 18 | export type IsAny = 0 extends 1 & T ? true : false; 19 | export type NotAny = true extends IsAny ? false : true; 20 | 21 | export type Debug = { [K in keyof T]: T[K] }; 22 | export type MergeInsertions = T extends object 23 | ? { [K in keyof T]: MergeInsertions } 24 | : T; 25 | 26 | export type Alike = Equal, MergeInsertions>; 27 | 28 | export type ExpectExtends = EXPECTED extends VALUE 29 | ? true 30 | : false; 31 | export type ExpectValidArgs< 32 | FUNC extends (...args: any[]) => any, 33 | ARGS extends any[] 34 | > = ARGS extends Parameters ? true : false; 35 | 36 | export type UnionToIntersection = ( 37 | U extends any ? (k: U) => void : never 38 | ) extends (k: infer I) => void 39 | ? I 40 | : never; 41 | -------------------------------------------------------------------------------- /src/__types__/typesTest.ts: -------------------------------------------------------------------------------- 1 | // Notice: 2 | // adapted from [@instantdb/react](https://github.com/instantdb/instant/blob/main/client/packages/react/README.md) 3 | // see instantdb-license.md for license 4 | 5 | import type { Equal, Expect, IsAny, NotAny } from "./typeUtils.ts"; 6 | import { i, init } from "../index"; 7 | 8 | const schema = i.schema({ 9 | entities: { 10 | tbl: i.entity({ 11 | d: i.date().indexed(), 12 | dOptional: i.date().optional(), 13 | }), 14 | }, 15 | }); 16 | 17 | function _testUseDatesTest() { 18 | const db = init({ 19 | schema, 20 | appId: "123", 21 | useDateObjects: true, 22 | }); 23 | 24 | const { data } = db.useQuery({ tbl: {} }); 25 | const item = data.value?.tbl[0]; 26 | if (item) { 27 | type t = typeof item.d; 28 | type _cases = [Expect>, Expect>]; 29 | 30 | type tOpt = typeof item.dOptional; 31 | type _cases_2 = [ 32 | Expect>, 33 | Expect> 34 | ]; 35 | } 36 | } 37 | 38 | function _testUseDatesFalseTest() { 39 | const db = init({ 40 | schema, 41 | appId: "123", 42 | useDateObjects: false, 43 | }); 44 | 45 | const { data } = db.useQuery({ tbl: {} }); 46 | const item = data.value?.tbl[0]; 47 | if (item) { 48 | type t = typeof item.d; 49 | type _cases = [Expect>, Expect>]; 50 | 51 | type tOpt = typeof item.dOptional; 52 | type _cases_2 = [ 53 | Expect>, 54 | Expect> 55 | ]; 56 | } 57 | } 58 | 59 | function _testUseDatesUndefinedTest() { 60 | const db = init({ 61 | schema, 62 | appId: "123", 63 | }); 64 | 65 | const { data } = db.useQuery({ tbl: {} }); 66 | const item = data.value?.tbl[0]; 67 | if (item) { 68 | type t = typeof item.d; 69 | type _cases = [Expect>, Expect>]; 70 | 71 | type tOpt = typeof item.dOptional; 72 | type _cases_2 = [ 73 | Expect>, 74 | Expect> 75 | ]; 76 | } 77 | } 78 | 79 | function _testDataNoSchema() { 80 | const db = init({ 81 | appId: "123", 82 | }); 83 | 84 | const { data } = db.useQuery({ tbl: {} }); 85 | const item = data.value?.tbl[0]; 86 | if (item) { 87 | type t = typeof item.d; 88 | type _cases = [Expect>, Expect>]; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/components/Cursor.vue: -------------------------------------------------------------------------------- 1 | // Notice: // adapted from 2 | [@instantdb/react](https://github.com/instantdb/instant/blob/main/client/packages/react/README.md) 3 | // see instantdb-license.md for license 4 | 5 | 14 | 15 | 45 | -------------------------------------------------------------------------------- /src/components/Cursors.vue: -------------------------------------------------------------------------------- 1 | // Notice: // adapted from 2 | [@instantdb/react](https://github.com/instantdb/instant/blob/main/client/packages/react/README.md) 3 | // see instantdb-license.md for license 4 | 5 | 179 | 180 | 225 | -------------------------------------------------------------------------------- /src/components/index.ts: -------------------------------------------------------------------------------- 1 | import Cursors from "./Cursors.vue"; 2 | 3 | interface CursorSchema { 4 | x: number; 5 | y: number; 6 | xPercent: number; 7 | yPercent: number; 8 | color: string; 9 | } 10 | 11 | export { Cursors }; 12 | export type { CursorSchema }; 13 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // Notice: 2 | // adapted from [@instantdb/react](https://github.com/instantdb/instant/blob/main/client/packages/react/README.md) 3 | // see instantdb-license.md for license 4 | 5 | import { 6 | id, 7 | tx, 8 | lookup, 9 | i, 10 | // error 11 | InstantAPIError, 12 | // sync table enums 13 | SyncTableCallbackEventType, 14 | } from "@instantdb/core"; 15 | import type { 16 | QueryResponse, 17 | InstantQuery, 18 | InstantQueryResult, 19 | InstantSchema, 20 | InstantObject, 21 | InstantEntity, 22 | InstantSchemaDatabase, 23 | InstantUnknownSchemaDef, 24 | IInstantDatabase, 25 | User, 26 | AuthState, 27 | Query, 28 | InstaQLParams, 29 | ConnectionStatus, 30 | ValidQuery, 31 | // presence types 32 | PresencePeer, 33 | // schema types 34 | AttrsDefs, 35 | CardinalityKind, 36 | DataAttrDef, 37 | EntitiesDef, 38 | EntitiesWithLinks, 39 | EntityDef, 40 | InstantGraph, 41 | LinkAttrDef, 42 | LinkDef, 43 | LinksDef, 44 | ResolveAttrs, 45 | ValueTypes, 46 | InstaQLEntity, 47 | InstaQLFields, 48 | InstaQLResult, 49 | InstaQLEntitySubquery, 50 | RoomsOf, 51 | RoomsDef, 52 | PresenceOf, 53 | TopicsOf, 54 | TopicOf, 55 | RoomHandle, 56 | TransactionChunk, 57 | InstantUnknownSchema, 58 | InstantSchemaDef, 59 | BackwardsCompatibleSchema, 60 | InstantRules, 61 | UpdateParams, 62 | LinkParams, 63 | CreateParams, 64 | ExchangeCodeForTokenParams, 65 | SendMagicCodeParams, 66 | SendMagicCodeResponse, 67 | SignInWithIdTokenParams, 68 | VerifyMagicCodeParams, 69 | VerifyResponse, 70 | // storage types 71 | FileOpts, 72 | UploadFileResponse, 73 | DeleteFileResponse, 74 | // sync table types 75 | SyncTableCallback, 76 | SyncTableCallbackEvent, 77 | SyncTableInitialSyncBatch, 78 | SyncTableInitialSyncComplete, 79 | SyncTableSyncTransaction, 80 | SyncTableLoadFromStorage, 81 | SyncTableSetupError, 82 | } from "@instantdb/core"; 83 | 84 | import InstantVueWebDatabase from "./InstantVueWebDatabase"; 85 | import InstantVueAbstractDatabase from "./InstantVueAbstractDatabase"; 86 | import { init, init_experimental } from "./init"; 87 | import type { InstantConfig } from "./init"; 88 | import type { CursorSchema } from "./components"; 89 | 90 | /** 91 | * @deprecated 92 | * Use `InstantVueWebDatabase` 93 | */ 94 | const InstantVueDatabase = InstantVueWebDatabase; 95 | 96 | export { 97 | id, 98 | tx, 99 | lookup, 100 | init, 101 | init_experimental, 102 | InstantVueWebDatabase, 103 | InstantVueDatabase, 104 | i, 105 | // error 106 | InstantAPIError, 107 | // internal 108 | InstantVueAbstractDatabase, 109 | // sync table enums 110 | SyncTableCallbackEventType, 111 | }; 112 | export type { 113 | InstantConfig, 114 | InstantUnknownSchemaDef, 115 | Query, 116 | QueryResponse, 117 | InstantObject, 118 | User, 119 | AuthState, 120 | ConnectionStatus, 121 | InstantQuery, 122 | InstantQueryResult, 123 | InstantSchema, 124 | InstantEntity, 125 | InstantSchemaDatabase, 126 | IInstantDatabase, 127 | InstaQLParams, 128 | ValidQuery, 129 | InstaQLFields, 130 | // presence types 131 | PresencePeer, 132 | // schema types 133 | AttrsDefs, 134 | CardinalityKind, 135 | DataAttrDef, 136 | EntitiesDef, 137 | EntitiesWithLinks, 138 | EntityDef, 139 | InstantGraph, 140 | LinkAttrDef, 141 | LinkDef, 142 | LinksDef, 143 | ResolveAttrs, 144 | ValueTypes, 145 | InstaQLEntity, 146 | InstaQLResult, 147 | InstaQLEntitySubquery, 148 | RoomsOf, 149 | TransactionChunk, 150 | RoomsDef, 151 | PresenceOf, 152 | TopicsOf, 153 | TopicOf, 154 | RoomHandle, 155 | InstantUnknownSchema, 156 | InstantSchemaDef, 157 | BackwardsCompatibleSchema, 158 | InstantRules, 159 | UpdateParams, 160 | LinkParams, 161 | CreateParams, 162 | ExchangeCodeForTokenParams, 163 | SendMagicCodeParams, 164 | SendMagicCodeResponse, 165 | SignInWithIdTokenParams, 166 | VerifyMagicCodeParams, 167 | VerifyResponse, 168 | // storage types 169 | FileOpts, 170 | UploadFileResponse, 171 | DeleteFileResponse, 172 | // sync table types 173 | SyncTableCallback, 174 | SyncTableCallbackEvent, 175 | SyncTableInitialSyncBatch, 176 | SyncTableInitialSyncComplete, 177 | SyncTableSyncTransaction, 178 | SyncTableLoadFromStorage, 179 | SyncTableSetupError, 180 | // 181 | CursorSchema, 182 | }; 183 | -------------------------------------------------------------------------------- /src/init.ts: -------------------------------------------------------------------------------- 1 | // Notice: 2 | // adapted from [@instantdb/react](https://github.com/instantdb/instant/blob/main/client/packages/react/README.md) 3 | // see instantdb-license.md for license 4 | 5 | import type { 6 | InstantConfig as OriginalInstantConfig, 7 | InstantSchemaDef, 8 | InstantUnknownSchema, 9 | } from "@instantdb/core"; 10 | import InstantVueWebDatabase from "./InstantVueWebDatabase"; 11 | import version from "./version"; 12 | 13 | export interface Extra { 14 | clientOnlyUseQuery: boolean; 15 | stopLoadingOnNullQuery: boolean; 16 | } 17 | 18 | type ExtraConfig = { 19 | __extra_vue?: Partial; 20 | }; 21 | 22 | type DeprecatedExtraConfig = Partial<{ 23 | /** @deprecated use __extra_vue.clientOnlyUseQuery instead */ 24 | clientOnlyUseQuery: boolean; 25 | }>; 26 | 27 | export type InstantConfig< 28 | S extends InstantSchemaDef, 29 | UseDates extends boolean = false 30 | > = OriginalInstantConfig & ExtraConfig & DeprecatedExtraConfig; 31 | 32 | /** 33 | * 34 | * The first step: init your application! 35 | * 36 | * Visit https://instantdb.com/dash to get your `appId` :) 37 | * 38 | * @example 39 | * import { init } from "@dorilama/instantdb-vue" 40 | * 41 | * const db = init({ appId: "my-app-id" }) 42 | * 43 | * // You can also provide a a schema for type safety and editor autocomplete! 44 | * 45 | * import { init } from "@dorilama/instantdb-vue" 46 | * import schema from ""../instant.schema.ts"; 47 | * 48 | * const db = init({ appId: "my-app-id", schema }) 49 | * 50 | * // To learn more: https://instantdb.com/docs/modeling-data 51 | * 52 | */ 53 | export function init< 54 | Schema extends InstantSchemaDef = InstantUnknownSchema, 55 | UseDates extends boolean = false 56 | >( 57 | // Allows config with missing `useDateObjects`, but keeps `UseDates` 58 | // as a non-nullable in the InstantConfig type. 59 | config: Omit, "useDateObjects"> & { 60 | useDateObjects?: UseDates; 61 | } 62 | ): InstantVueWebDatabase> { 63 | const configStrict = { 64 | ...config, 65 | useDateObjects: (config.useDateObjects ?? false) as UseDates, 66 | }; 67 | return new InstantVueWebDatabase< 68 | Schema, 69 | UseDates, 70 | InstantConfig 71 | >(configStrict, { 72 | "@dorilama/instantdb-vue": version, 73 | }); 74 | } 75 | 76 | /** 77 | * @deprecated 78 | * `init_experimental` is deprecated. You can replace it with `init`. 79 | * 80 | * @example 81 | * 82 | * // Before 83 | * import { init_experimental } from "@dorilama/instantdb-vue" 84 | * const db = init_experimental({ ... }); 85 | * 86 | * // After 87 | * import { init } from "@dorilama/instantdb-vue" 88 | * const db = init({ ... }); 89 | */ 90 | export const init_experimental = init; 91 | -------------------------------------------------------------------------------- /src/useQuery.ts: -------------------------------------------------------------------------------- 1 | // Notice: 2 | // adapted from [@instantdb/react](https://github.com/instantdb/instant/blob/main/client/packages/react/README.md) 3 | // see instantdb-license.md for license 4 | 5 | import { weakHash, coerceQuery, InstantCoreDatabase } from "@instantdb/core"; 6 | import type { 7 | InstaQLOptions, 8 | InstaQLLifecycleState, 9 | InstantSchemaDef, 10 | ValidQuery, 11 | } from "@instantdb/core"; 12 | import { shallowRef, computed, toValue, watch, ref } from "vue"; 13 | import type { ShallowRef, MaybeRefOrGetter } from "vue"; 14 | import { tryOnScopeDispose } from "./utils"; 15 | import type { Extra } from "./init"; 16 | 17 | export type UseQueryInternalReturn = { 18 | [K in keyof InstaQLLifecycleState]: ShallowRef< 19 | InstaQLLifecycleState[K] 20 | >; 21 | } & { stop: () => void }; 22 | 23 | function stateForResult(result: any) { 24 | return { 25 | isLoading: !Boolean(result), 26 | data: undefined, 27 | pageInfo: undefined, 28 | error: undefined, 29 | ...(result ? result : {}), 30 | }; 31 | } 32 | 33 | export function useQueryInternal< 34 | Q extends ValidQuery, 35 | Schema extends InstantSchemaDef, 36 | UseDates extends boolean 37 | >( 38 | _core: InstantCoreDatabase, 39 | _query: MaybeRefOrGetter, 40 | _opts?: MaybeRefOrGetter, 41 | extra?: Extra 42 | ): { 43 | state: UseQueryInternalReturn; 44 | query: any; 45 | } { 46 | const query = computed(() => { 47 | let __query = toValue(_query); 48 | const __opts = toValue(_opts); 49 | if (__query && __opts && "ruleParams" in __opts) { 50 | __query = { $$ruleParams: __opts["ruleParams"], ...__query }; 51 | } 52 | return __query ? coerceQuery(__query) : null; 53 | }); 54 | const queryHash = computed(() => { 55 | return weakHash(query.value); 56 | }); 57 | 58 | const initialState = stateForResult( 59 | _core._reactor.getPreviousResult(query.value) 60 | ); 61 | 62 | const state: UseQueryInternalReturn = { 63 | isLoading: ref(initialState.isLoading), 64 | data: shallowRef(initialState.data), 65 | pageInfo: shallowRef(initialState.pageInfo), 66 | error: shallowRef(initialState.error), 67 | stop: () => {}, 68 | }; 69 | 70 | if (!extra?.clientOnlyUseQuery || _core._reactor.querySubs) { 71 | const stop = watch( 72 | queryHash, 73 | (_, __, onCleanup) => { 74 | const currentState = stateForResult( 75 | _core._reactor.getPreviousResult(query.value) 76 | ); 77 | state.isLoading.value = currentState.isLoading; 78 | state.data.value = currentState.data; 79 | state.pageInfo.value = currentState.pageInfo; 80 | state.error.value = currentState.error; 81 | 82 | if (!query.value) { 83 | if (extra?.stopLoadingOnNullQuery) { 84 | state.isLoading.value = false; 85 | } 86 | return; 87 | } 88 | const unsubscribe = _core.subscribeQuery( 89 | query.value, 90 | (result) => { 91 | state.isLoading.value = !Boolean(result); 92 | state.data.value = result.data; 93 | state.pageInfo.value = result.pageInfo; 94 | state.error.value = result.error; 95 | } 96 | ); 97 | onCleanup(unsubscribe); 98 | }, 99 | { immediate: true } 100 | ); 101 | 102 | state.stop = stop; 103 | 104 | tryOnScopeDispose(() => { 105 | stop(); 106 | }); 107 | } 108 | 109 | return { state, query }; 110 | } 111 | -------------------------------------------------------------------------------- /src/useTimeout.ts: -------------------------------------------------------------------------------- 1 | // Notice: 2 | // adapted from [@instantdb/react](https://github.com/instantdb/instant/blob/main/client/packages/react/README.md) 3 | // see instantdb-license.md for license 4 | 5 | import { tryOnScopeDispose } from "./utils"; 6 | 7 | export function useTimeout() { 8 | let timeout: ReturnType | null = null; 9 | 10 | function set(delay: number, fn: () => void) { 11 | clear(); 12 | 13 | timeout = setTimeout(fn, delay); 14 | } 15 | 16 | function clear() { 17 | if (timeout) { 18 | clearTimeout(timeout); 19 | } 20 | } 21 | 22 | tryOnScopeDispose(() => { 23 | clear(); 24 | }); 25 | 26 | return { set, clear }; 27 | } 28 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { onScopeDispose, getCurrentScope } from "vue"; 2 | 3 | /* 4 | MIT License 5 | 6 | Copyright (c) 2019-PRESENT Anthony Fu 7 | */ 8 | export function tryOnScopeDispose(fn: () => void) { 9 | if (getCurrentScope()) { 10 | onScopeDispose(fn); 11 | return true; 12 | } 13 | return false; 14 | } 15 | 16 | export type Arrayable = T[] | T; 17 | -------------------------------------------------------------------------------- /src/version.ts: -------------------------------------------------------------------------------- 1 | // Autogenerated by prepublish.js 2 | const version = "v0.9.0"; 3 | export default version; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.dom.json" 3 | } 4 | --------------------------------------------------------------------------------