├── .prettierignore ├── .prettierrc.json ├── styles ├── globals.css └── Home.module.css ├── postcss.config.js ├── tailwind.config.js ├── pages ├── _app.tsx └── index.tsx ├── hooks ├── useTypingIndicator.ts ├── useSessionStorage.ts ├── useLatestValue.ts ├── useSingleFlight.ts └── usePresence.ts ├── .gitignore ├── convex ├── _generated │ ├── api.js │ ├── api.d.ts │ ├── dataModel.d.ts │ ├── server.js │ └── server.d.ts ├── schema.ts ├── tsconfig.json ├── README.md └── presence.ts ├── tsconfig.json ├── package.json ├── components ├── Facepile.tsx └── SharedCursors.tsx ├── public ├── favicon.svg └── convex.svg ├── .eslintrc.js ├── README.md └── LICENSE /.prettierignore: -------------------------------------------------------------------------------- 1 | .next 2 | convex/_generated 3 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } 4 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | './app/**/*.{js,ts,jsx,tsx}', 5 | './pages/**/*.{js,ts,jsx,tsx}', 6 | './components/**/*.{js,ts,jsx,tsx}', 7 | ], 8 | theme: { 9 | extend: {}, 10 | }, 11 | plugins: [], 12 | } 13 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import '../styles/globals.css'; 2 | import type { AppProps } from 'next/app'; 3 | 4 | import { ConvexProvider, ConvexReactClient } from 'convex/react'; 5 | const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!); 6 | 7 | function MyApp({ Component, pageProps }: AppProps) { 8 | return ( 9 | 10 | 11 | 12 | ); 13 | } 14 | 15 | export default MyApp; 16 | -------------------------------------------------------------------------------- /hooks/useTypingIndicator.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | 3 | export default ( 4 | text: string, 5 | updateMyPresence: (p: { typing?: boolean }) => void 6 | ) => { 7 | useEffect(() => { 8 | if (text.length === 0) { 9 | updateMyPresence({ typing: false }); 10 | return; 11 | } 12 | updateMyPresence({ typing: true }); 13 | const timer = setTimeout(() => updateMyPresence({ typing: false }), 1000); 14 | return () => clearTimeout(timer); 15 | }, [updateMyPresence, text]); 16 | }; 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /convex/_generated/api.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * Generated `api` utility. 4 | * 5 | * THIS CODE IS AUTOMATICALLY GENERATED. 6 | * 7 | * Generated by convex@1.11.0. 8 | * To regenerate, run `npx convex dev`. 9 | * @module 10 | */ 11 | 12 | import { anyApi } from "convex/server"; 13 | 14 | /** 15 | * A utility for referencing Convex functions in your app's API. 16 | * 17 | * Usage: 18 | * ```js 19 | * const myFunctionReference = api.myModule.myFunction; 20 | * ``` 21 | */ 22 | export const api = anyApi; 23 | export const internal = anyApi; 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true 17 | }, 18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /convex/schema.ts: -------------------------------------------------------------------------------- 1 | import { defineSchema, defineTable } from 'convex/server'; 2 | import { v } from 'convex/values'; 3 | 4 | export default defineSchema({ 5 | presence: defineTable({ 6 | user: v.string(), 7 | room: v.string(), 8 | present: v.boolean(), 9 | latestJoin: v.number(), 10 | data: v.any(), 11 | }) 12 | .index('room_present_join', ['room', 'present', 'latestJoin']) 13 | .index('room_user', ['room', 'user']), 14 | 15 | presence_heartbeats: defineTable({ 16 | user: v.string(), 17 | room: v.string(), 18 | markAsGone: v.id('_scheduled_functions'), 19 | }).index('by_room_user', ['room', 'user']), 20 | }); 21 | -------------------------------------------------------------------------------- /convex/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | /* This TypeScript project config describes the environment that 3 | * Convex functions run in and is used to typecheck them. 4 | * You can modify it, but some settings required to use Convex. 5 | */ 6 | "compilerOptions": { 7 | /* These settings are not required by Convex and can be modified. */ 8 | "allowJs": true, 9 | "strict": true, 10 | 11 | /* These compiler options are required by Convex */ 12 | "target": "ESNext", 13 | "lib": ["ES2021", "dom"], 14 | "forceConsistentCasingInFileNames": true, 15 | "allowSyntheticDefaultImports": true, 16 | "skipLibCheck": true, 17 | "module": "ESNext", 18 | "moduleResolution": "Node", 19 | "isolatedModules": true, 20 | "noEmit": true 21 | }, 22 | "include": ["./**/*"], 23 | "exclude": ["./_generated"] 24 | } 25 | -------------------------------------------------------------------------------- /convex/_generated/api.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * Generated `api` utility. 4 | * 5 | * THIS CODE IS AUTOMATICALLY GENERATED. 6 | * 7 | * Generated by convex@1.11.0. 8 | * To regenerate, run `npx convex dev`. 9 | * @module 10 | */ 11 | 12 | import type { 13 | ApiFromModules, 14 | FilterApi, 15 | FunctionReference, 16 | } from "convex/server"; 17 | import type * as presence from "../presence.js"; 18 | 19 | /** 20 | * A utility for referencing Convex functions in your app's API. 21 | * 22 | * Usage: 23 | * ```js 24 | * const myFunctionReference = api.myModule.myFunction; 25 | * ``` 26 | */ 27 | declare const fullApi: ApiFromModules<{ 28 | presence: typeof presence; 29 | }>; 30 | export declare const api: FilterApi< 31 | typeof fullApi, 32 | FunctionReference 33 | >; 34 | export declare const internal: FilterApi< 35 | typeof fullApi, 36 | FunctionReference 37 | >; 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "dev": "npm-run-all --parallel dev:backend dev:frontend", 5 | "predev": "convex dev --until-success", 6 | "dev:backend": "convex dev", 7 | "dev:frontend": "next dev", 8 | "build": "next build", 9 | "start": "next start", 10 | "lint": "next lint" 11 | }, 12 | "dependencies": { 13 | "classnames": "^2.3.2", 14 | "convex": "^1.11.0", 15 | "next": "latest", 16 | "react": "^18.2.0", 17 | "react-dom": "^18.2.0" 18 | }, 19 | "devDependencies": { 20 | "@types/node": "^20.4.4", 21 | "@types/react": "^18.2.78", 22 | "@types/react-dom": "^18.2.25", 23 | "@typescript-eslint/eslint-plugin": "^5.43.0", 24 | "autoprefixer": "^10.4.13", 25 | "eslint-plugin-react": "^7.31.11", 26 | "eslint-plugin-react-hooks": "^4.6.0", 27 | "npm-run-all": "^4.1.5", 28 | "postcss": "^8.4.18", 29 | "prettier": "^2.7.1", 30 | "tailwindcss": "^3.2.3", 31 | "typescript": "^4.7.3" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /hooks/useSessionStorage.ts: -------------------------------------------------------------------------------- 1 | import { convexToJson, jsonToConvex, Value } from 'convex/values'; 2 | import { useCallback, useState } from 'react'; 3 | 4 | function useSessionStorage( 5 | key: string 6 | ): [T | undefined, (value: T) => void]; 7 | function useSessionStorage( // eslint-disable-line no-redeclare 8 | key: string, 9 | defaultValue: T | (() => T) 10 | ): [T, (value: T) => void]; 11 | function useSessionStorage( // eslint-disable-line no-redeclare 12 | key: string, 13 | defaultValue?: T | (() => T) 14 | ) { 15 | const [value, setValueInternal] = useState(() => { 16 | if (typeof sessionStorage !== 'undefined') { 17 | const existing = sessionStorage.getItem(key); 18 | if (existing) { 19 | try { 20 | return jsonToConvex(JSON.parse(existing)) as T; 21 | } catch (e) { 22 | console.error(e); 23 | } 24 | } 25 | } 26 | if (typeof defaultValue === 'function') { 27 | return defaultValue(); 28 | } 29 | return defaultValue; 30 | }); 31 | const setValue = useCallback( 32 | (value: T) => { 33 | sessionStorage.setItem(key, JSON.stringify(convexToJson(value))); 34 | setValueInternal(value); 35 | }, 36 | [key] 37 | ); 38 | return [value, setValue] as const; 39 | } 40 | 41 | export default useSessionStorage; 42 | -------------------------------------------------------------------------------- /hooks/useLatestValue.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useMemo, useRef } from 'react'; 2 | 3 | /** 4 | * Promise-based access to the latest value updated. 5 | * Every call to nextValue will return a promise to the next value. 6 | * "Next value" is defined as the latest value passed to "updateValue" that 7 | * hasn not been returned yet. 8 | * @returns a function to await for a new value, and one to update the value. 9 | */ 10 | export default function useLatestValue() { 11 | const initial = useMemo(() => { 12 | const [promise, resolve] = makeSignal(); 13 | // We won't access data until it has been updated. 14 | return { data: undefined as T, promise, resolve }; 15 | }, []); 16 | const ref = useRef(initial); 17 | const nextValue = useCallback(async () => { 18 | await ref.current.promise; 19 | const [promise, resolve] = makeSignal(); 20 | ref.current.promise = promise; 21 | ref.current.resolve = resolve; 22 | return ref.current.data; 23 | }, [ref]); 24 | 25 | const updateValue = useCallback( 26 | (data: T) => { 27 | ref.current.data = data; 28 | ref.current.resolve(); 29 | }, 30 | [ref] 31 | ); 32 | 33 | return [nextValue, updateValue] as const; 34 | } 35 | 36 | const makeSignal = () => { 37 | let resolve: () => void; 38 | const promise = new Promise((r) => (resolve = r)); 39 | return [promise, resolve!] as const; 40 | }; 41 | -------------------------------------------------------------------------------- /components/Facepile.tsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import { useEffect, useState } from 'react'; 3 | import { PresenceData } from '../hooks/usePresence'; 4 | 5 | const UPDATE_MS = 1000; 6 | 7 | type FacePileProps = { 8 | othersPresence?: PresenceData<{ emoji: string }>[]; 9 | }; 10 | export default ({ othersPresence }: FacePileProps) => { 11 | const [, setNow] = useState(Date.now()); 12 | useEffect(() => { 13 | const intervalId = setInterval(() => setNow(Date.now()), UPDATE_MS); 14 | return () => clearInterval(intervalId); 15 | }, [setNow]); 16 | return ( 17 |
18 | {othersPresence 19 | ?.slice(0, 5) 20 | .map((presence) => ({ 21 | ...presence, 22 | online: presence.present, 23 | })) 24 | .sort((presence1, presence2) => 25 | presence1.online === presence2.online 26 | ? presence1.created - presence2.created 27 | : Number(presence1.online) - Number(presence2.online) 28 | ) 29 | .map((presence) => ( 30 | 42 | {presence.data.emoji} 43 | 44 | ))} 45 |
46 | ); 47 | }; 48 | -------------------------------------------------------------------------------- /public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /components/SharedCursors.tsx: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react'; 2 | import { PresenceData } from '../hooks/usePresence'; 3 | 4 | type Data = { 5 | text: string; 6 | emoji: string; 7 | x: number; 8 | y: number; 9 | typing: boolean; 10 | }; 11 | type SharedCursorsProps = { 12 | myPresenceData: Data; 13 | othersPresence?: PresenceData[]; 14 | updatePresence: (p: Partial) => void; 15 | }; 16 | export default ({ 17 | myPresenceData, 18 | othersPresence, 19 | updatePresence, 20 | }: SharedCursorsProps) => { 21 | const ref = useRef(null); 22 | return ( 23 |
{ 27 | const { x, y } = ref.current!.getBoundingClientRect(); 28 | void updatePresence({ x: e.clientX - x, y: e.clientY - y }); 29 | }} 30 | > 31 | 40 | {myPresenceData.emoji + ' ' + myPresenceData.text} 41 | 42 | {othersPresence 43 | ?.filter((p) => p.present) 44 | .filter((presence) => presence.data.x && presence.data.y) 45 | .map((presence) => ( 46 | 55 | {presence.data.emoji + ' ' + presence.data.text} 56 | 57 | ))} 58 |
59 | ); 60 | }; 61 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // means don't look to parent dir, so use `root: true` in descendant directories to ignore this config 3 | // See https://eslint.org/docs/user-guide/configuring/configuration-files#cascading-and-hierarchy 4 | root: true, 5 | parser: '@typescript-eslint/parser', 6 | parserOptions: { 7 | project: ['./tsconfig.json', './src/cli/tsconfig.json'], 8 | tsconfigRootDir: __dirname, 9 | }, 10 | plugins: ['@typescript-eslint', 'react-hooks', 'react'], 11 | extends: ['eslint:recommended'], 12 | env: { 13 | amd: true, 14 | browser: true, 15 | jest: true, 16 | node: true, 17 | }, 18 | rules: { 19 | 'no-debugger': 'error', 20 | // any is terrible but we use it a lot (even in our public code). 21 | '@typescript-eslint/no-explicit-any': 'off', 22 | 23 | // asserting that values aren't null is risky but useful. 24 | '@typescript-eslint/no-non-null-assertion': 'off', 25 | 26 | // allow (_arg: number) => {} 27 | '@typescript-eslint/no-unused-vars': [ 28 | 'error', 29 | { 30 | argsIgnorePattern: '^_', 31 | varsIgnorePattern: '^_', 32 | }, 33 | ], 34 | 35 | // Add React hooks rules so we don't misuse them. 36 | 'react-hooks/rules-of-hooks': 'error', 37 | 'react-hooks/exhaustive-deps': 'warn', 38 | 39 | // From https://github.com/typescript-eslint/typescript-eslint/issues/1391#issuecomment-1124154589 40 | // Prefer `private` ts keyword to `#private` private methods 41 | 'no-restricted-syntax': [ 42 | 'error', 43 | { 44 | selector: 45 | ':matches(PropertyDefinition, MethodDefinition) > PrivateIdentifier.key', 46 | message: 'Use `private` instead', 47 | }, 48 | ], 49 | // Makes it harder to accidentally fire off a promise without waiting for it. 50 | '@typescript-eslint/no-floating-promises': 'error', 51 | }, 52 | ignorePatterns: ['node_modules', 'dist', '*.js'], 53 | }; 54 | -------------------------------------------------------------------------------- /convex/_generated/dataModel.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * Generated data model types. 4 | * 5 | * THIS CODE IS AUTOMATICALLY GENERATED. 6 | * 7 | * Generated by convex@1.11.0. 8 | * To regenerate, run `npx convex dev`. 9 | * @module 10 | */ 11 | 12 | import type { 13 | DataModelFromSchemaDefinition, 14 | DocumentByName, 15 | TableNamesInDataModel, 16 | SystemTableNames, 17 | } from "convex/server"; 18 | import type { GenericId } from "convex/values"; 19 | import schema from "../schema.js"; 20 | 21 | /** 22 | * The names of all of your Convex tables. 23 | */ 24 | export type TableNames = TableNamesInDataModel; 25 | 26 | /** 27 | * The type of a document stored in Convex. 28 | * 29 | * @typeParam TableName - A string literal type of the table name (like "users"). 30 | */ 31 | export type Doc = DocumentByName< 32 | DataModel, 33 | TableName 34 | >; 35 | 36 | /** 37 | * An identifier for a document in Convex. 38 | * 39 | * Convex documents are uniquely identified by their `Id`, which is accessible 40 | * on the `_id` field. To learn more, see [Document IDs](https://docs.convex.dev/using/document-ids). 41 | * 42 | * Documents can be loaded using `db.get(id)` in query and mutation functions. 43 | * 44 | * IDs are just strings at runtime, but this type can be used to distinguish them from other 45 | * strings when type checking. 46 | * 47 | * @typeParam TableName - A string literal type of the table name (like "users"). 48 | */ 49 | export type Id = 50 | GenericId; 51 | 52 | /** 53 | * A type describing your Convex data model. 54 | * 55 | * This type includes information about what tables you have, the type of 56 | * documents stored in those tables, and the indexes defined on them. 57 | * 58 | * This type is used to parameterize methods like `queryGeneric` and 59 | * `mutationGeneric` to make them type-safe. 60 | */ 61 | export type DataModel = DataModelFromSchemaDefinition; 62 | -------------------------------------------------------------------------------- /styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | padding: 0 2rem; 3 | display: flex; 4 | flex-direction: column; 5 | min-height: 100vh; 6 | } 7 | 8 | .main { 9 | padding: 4rem 0; 10 | flex: 10; 11 | display: flex; 12 | flex-direction: column; 13 | justify-content: center; 14 | align-items: center; 15 | } 16 | 17 | .button { 18 | font-size: 1rem; 19 | font-weight: 800; 20 | cursor: pointer; 21 | margin: 0.5rem; 22 | padding: 0.5rem; 23 | text-align: left; 24 | text-decoration: none; 25 | border: 1px solid #eaeaea; 26 | border-radius: 10px; 27 | transition: color 0.15s ease, border-color 0.15s ease; 28 | text-align: center; 29 | width: 200px; 30 | } 31 | 32 | .button:hover, 33 | .button:focus, 34 | .button:active { 35 | color: #0070f3; 36 | border-color: #0070f3; 37 | } 38 | 39 | .footer { 40 | display: flex; 41 | flex: 1; 42 | padding: 2rem 0; 43 | border-top: 1px solid #eaeaea; 44 | justify-content: center; 45 | align-items: center; 46 | } 47 | 48 | .footer a { 49 | display: flex; 50 | justify-content: center; 51 | align-items: center; 52 | flex-grow: 1; 53 | } 54 | 55 | .title a { 56 | color: #0070f3; 57 | text-decoration: none; 58 | } 59 | 60 | .title a:hover, 61 | .title a:focus, 62 | .title a:active { 63 | text-decoration: underline; 64 | } 65 | 66 | .title { 67 | margin: 0; 68 | line-height: 1.15; 69 | font-size: 3rem; 70 | } 71 | 72 | .title, 73 | .description { 74 | text-align: center; 75 | } 76 | 77 | .description { 78 | margin: 2rem 0; 79 | line-height: 1.5; 80 | font-size: 1.5rem; 81 | } 82 | 83 | .logo { 84 | height: 1em; 85 | margin-left: 0.5rem; 86 | } 87 | 88 | .loadingLayout { 89 | display: flex; 90 | height: 100vh; 91 | } 92 | 93 | .loading, 94 | .loading:after { 95 | border-radius: 50%; 96 | width: 5em; 97 | height: 5em; 98 | } 99 | .loading { 100 | margin: auto auto; 101 | border: 0.8em solid #f4e9f1; 102 | border-left: 0.8em solid #8d2676; 103 | animation: load8 1.1s infinite linear; 104 | } 105 | @keyframes load8 { 106 | 0% { 107 | transform: rotate(0deg); 108 | } 109 | 100% { 110 | transform: rotate(360deg); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /hooks/useSingleFlight.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useRef } from 'react'; 2 | 3 | /** 4 | * Wraps a function to single-flight invocations, using the latest args. 5 | * 6 | * Generates a function that behaves like the passed in function, 7 | * but only one execution runs at a time. If multiple calls are requested 8 | * before the current call has finished, it will use the latest arguments 9 | * for the next invocation. 10 | * 11 | * Note: some requests may never be made. If while a request is in-flight, N 12 | * requests are made, N-1 of them will never resolve or reject the promise they 13 | * returned. For most applications this is the desired behavior, but if you need 14 | * all calls to eventually resolve, you can modify this code. Some behavior you 15 | * could add, left as an exercise to the reader: 16 | * 1. Resolve with the previous result when a request is about to be dropped. 17 | * 2. Resolve all N requests with the result of the next request. 18 | * 3. Do not return anything, and use this as a fire-and-forget library only. 19 | * 20 | * @param fn - Function to be called, with only one request in flight at a time. 21 | * This must be a stable identifier, e.g. returned from useCallback. 22 | * @returns Function that can be called whenever, returning a promise that will 23 | * only resolve or throw if the underlying function gets called. 24 | */ 25 | export default function useSingleFlight< 26 | F extends (...args: any[]) => Promise 27 | >(fn: F) { 28 | const flightStatus = useRef({ 29 | inFlight: false, 30 | upNext: null as null | { 31 | fn: F; 32 | resolve: any; 33 | reject: any; 34 | args: Parameters; 35 | }, 36 | }); 37 | 38 | return useCallback( 39 | (...args: Parameters): ReturnType => { 40 | if (flightStatus.current.inFlight) { 41 | return new Promise((resolve, reject) => { 42 | flightStatus.current.upNext = { fn, resolve, reject, args }; 43 | }) as ReturnType; 44 | } 45 | flightStatus.current.inFlight = true; 46 | const firstReq = fn(...args) as ReturnType; 47 | void (async () => { 48 | try { 49 | await firstReq; 50 | } finally { 51 | // If it failed, we naively just move on to the next request. 52 | } 53 | while (flightStatus.current.upNext) { 54 | let cur = flightStatus.current.upNext; 55 | flightStatus.current.upNext = null; 56 | await cur 57 | .fn(...cur.args) 58 | .then(cur.resolve) 59 | .catch(cur.reject); 60 | } 61 | flightStatus.current.inFlight = false; 62 | })(); 63 | return firstReq; 64 | }, 65 | [fn] 66 | ); 67 | } 68 | -------------------------------------------------------------------------------- /convex/README.md: -------------------------------------------------------------------------------- 1 | # Welcome to your Convex functions directory! 2 | 3 | Write your Convex functions here. See 4 | https://docs.convex.dev/using/writing-convex-functions for more. 5 | 6 | A query function that takes two arguments looks like: 7 | 8 | ```ts 9 | // functions.js 10 | import { query } from "./_generated/server"; 11 | import { v } from "convex/values"; 12 | 13 | export const myQueryFunction = query({ 14 | // Validators for arguments. 15 | args: { 16 | first: v.number(), 17 | second: v.string(), 18 | }, 19 | 20 | // Function implementation. 21 | hander: async (ctx, args) => { 22 | // Read the database as many times as you need here. 23 | // See https://docs.convex.dev/database/reading-data. 24 | const documents = await ctx.db.query("tablename").collect(); 25 | 26 | // Arguments passed from the client are properties of the args object. 27 | console.log(args.first, args.second); 28 | 29 | // Write arbitrary JavaScript here: filter, aggregate, build derived data, 30 | // remove non-public properties, or create new objects. 31 | return documents; 32 | }, 33 | }); 34 | ``` 35 | 36 | Using this query function in a React component looks like: 37 | 38 | ```ts 39 | const data = useQuery(api.functions.myQueryFunction, { 40 | first: 10, 41 | second: "hello", 42 | }); 43 | ``` 44 | 45 | A mutation function looks like: 46 | 47 | ```ts 48 | // functions.js 49 | import { mutation } from "./_generated/server"; 50 | import { v } from "convex/values"; 51 | 52 | export const myMutationFunction = mutation({ 53 | // Validators for arguments. 54 | args: { 55 | first: v.string(), 56 | second: v.string(), 57 | }, 58 | 59 | // Function implementation. 60 | hander: async (ctx, args) => { 61 | // Insert or modify documents in the database here. 62 | // Mutations can also read from the database like queries. 63 | // See https://docs.convex.dev/database/writing-data. 64 | const message = { body: args.first, author: args.second }; 65 | const id = await ctx.db.insert("messages", message); 66 | 67 | // Optionally, return a value from your mutation. 68 | return await ctx.db.get(id); 69 | }, 70 | }); 71 | ``` 72 | 73 | Using this mutation function in a React component looks like: 74 | 75 | ```ts 76 | const mutation = useMutation(api.functions.myMutationFunction); 77 | function handleButtonPress() { 78 | // fire and forget, the most common way to use mutations 79 | mutation({ first: "Hello!", second: "me" }); 80 | // OR 81 | // use the result once the mutation has completed 82 | mutation({ first: "Hello!", second: "me" }).then((result) => 83 | console.log(result) 84 | ); 85 | } 86 | ``` 87 | 88 | Use the Convex CLI to push your functions to a deployment. See everything 89 | the Convex CLI can do by running `npx convex -h` in your project root 90 | directory. To learn more, launch the docs with `npx convex docs`. 91 | -------------------------------------------------------------------------------- /hooks/usePresence.ts: -------------------------------------------------------------------------------- 1 | import { api } from '../convex/_generated/api'; 2 | import { useQuery, useMutation } from 'convex/react'; 3 | import { Value } from 'convex/values'; 4 | import { useCallback, useEffect, useState } from 'react'; 5 | import useSingleFlight from './useSingleFlight'; 6 | 7 | export type PresenceData = { 8 | created: number; 9 | latestJoin: number; 10 | user: string; 11 | data: D; 12 | present: boolean; 13 | }; 14 | 15 | const HEARTBEAT_PERIOD = 5000; 16 | 17 | /** 18 | * usePresence is a React hook for reading & writing presence data. 19 | * 20 | * The data is written by various users, and comes back as a list of data for 21 | * other users in the same room. It is not meant for mission-critical data, but 22 | * rather for optimistic metadata, like whether a user is online, typing, or 23 | * at a certain location on a page. The data is single-flighted, and when many 24 | * updates are requested while an update is in flight, only the latest data will 25 | * be sent in the next request. See for more details on single-flighting: 26 | * https://stack.convex.dev/throttling-requests-by-single-flighting 27 | * 28 | * Data updates are merged with previous data. This data will reflect all 29 | * updates, not just the data that gets synchronized to the server. So if you 30 | * update with {mug: userMug} and {typing: true}, the data will have both 31 | * `mug` and `typing` fields set, and will be immediately reflected in the data 32 | * returned as the first parameter. 33 | * 34 | * @param room - The location associated with the presence data. Examples: 35 | * page, chat channel, game instance. 36 | * @param user - The user associated with the presence data. 37 | * @param initialData - The initial data to associate with the user. 38 | * @param heartbeatPeriod? - If specified, the interval between heartbeats, in 39 | * milliseconds. A heartbeat updates the user's presence "updated" timestamp. 40 | * The faster the updates, the more quickly you can detect a user "left" at 41 | * the cost of more server function calls. 42 | * @returns A list with 1. this user's data; 2. A list of other users' data; 43 | * 3. function to update this user's data. It will do a shallow merge. 44 | */ 45 | export const usePresence = ( 46 | room: string, 47 | user: string, 48 | initialData: T, 49 | heartbeatPeriod = HEARTBEAT_PERIOD 50 | ) => { 51 | const [data, setData] = useState(initialData); 52 | let presence: PresenceData[] | undefined = useQuery(api.presence.list, { 53 | room, 54 | }); 55 | if (presence) { 56 | presence = presence.filter((p) => p.user !== user); 57 | } 58 | const updatePresence = useSingleFlight(useMutation(api.presence.update)); 59 | const heartbeat = useSingleFlight(useMutation(api.presence.heartbeat)); 60 | 61 | // Initial update and signal departure when we leave. 62 | useEffect(() => { 63 | void updatePresence({ room, user, data }); 64 | }, []); 65 | 66 | useEffect(() => { 67 | const intervalId = setInterval(() => { 68 | void heartbeat({ room, user }); 69 | }, heartbeatPeriod); 70 | // Whenever we have any data change, it will get cleared. 71 | return () => clearInterval(intervalId); 72 | }, [heartbeat, room, user, heartbeatPeriod]); 73 | 74 | // Updates the data, merged with previous data state. 75 | const updateData = useCallback( 76 | (patch: Partial) => { 77 | setData((prevState) => { 78 | const data = { ...prevState, ...patch }; 79 | void updatePresence({ room, user, data }); 80 | return data; 81 | }); 82 | }, 83 | [room, user, updatePresence] 84 | ); 85 | 86 | return [data, presence, updateData] as const; 87 | }; 88 | 89 | export default usePresence; 90 | -------------------------------------------------------------------------------- /convex/_generated/server.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * Generated utilities for implementing server-side Convex query and mutation functions. 4 | * 5 | * THIS CODE IS AUTOMATICALLY GENERATED. 6 | * 7 | * Generated by convex@1.11.0. 8 | * To regenerate, run `npx convex dev`. 9 | * @module 10 | */ 11 | 12 | import { 13 | actionGeneric, 14 | httpActionGeneric, 15 | queryGeneric, 16 | mutationGeneric, 17 | internalActionGeneric, 18 | internalMutationGeneric, 19 | internalQueryGeneric, 20 | } from "convex/server"; 21 | 22 | /** 23 | * Define a query in this Convex app's public API. 24 | * 25 | * This function will be allowed to read your Convex database and will be accessible from the client. 26 | * 27 | * @param func - The query function. It receives a {@link QueryCtx} as its first argument. 28 | * @returns The wrapped query. Include this as an `export` to name it and make it accessible. 29 | */ 30 | export const query = queryGeneric; 31 | 32 | /** 33 | * Define a query that is only accessible from other Convex functions (but not from the client). 34 | * 35 | * This function will be allowed to read from your Convex database. It will not be accessible from the client. 36 | * 37 | * @param func - The query function. It receives a {@link QueryCtx} as its first argument. 38 | * @returns The wrapped query. Include this as an `export` to name it and make it accessible. 39 | */ 40 | export const internalQuery = internalQueryGeneric; 41 | 42 | /** 43 | * Define a mutation in this Convex app's public API. 44 | * 45 | * This function will be allowed to modify your Convex database and will be accessible from the client. 46 | * 47 | * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. 48 | * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. 49 | */ 50 | export const mutation = mutationGeneric; 51 | 52 | /** 53 | * Define a mutation that is only accessible from other Convex functions (but not from the client). 54 | * 55 | * This function will be allowed to modify your Convex database. It will not be accessible from the client. 56 | * 57 | * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. 58 | * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. 59 | */ 60 | export const internalMutation = internalMutationGeneric; 61 | 62 | /** 63 | * Define an action in this Convex app's public API. 64 | * 65 | * An action is a function which can execute any JavaScript code, including non-deterministic 66 | * code and code with side-effects, like calling third-party services. 67 | * They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive. 68 | * They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}. 69 | * 70 | * @param func - The action. It receives an {@link ActionCtx} as its first argument. 71 | * @returns The wrapped action. Include this as an `export` to name it and make it accessible. 72 | */ 73 | export const action = actionGeneric; 74 | 75 | /** 76 | * Define an action that is only accessible from other Convex functions (but not from the client). 77 | * 78 | * @param func - The function. It receives an {@link ActionCtx} as its first argument. 79 | * @returns The wrapped function. Include this as an `export` to name it and make it accessible. 80 | */ 81 | export const internalAction = internalActionGeneric; 82 | 83 | /** 84 | * Define a Convex HTTP action. 85 | * 86 | * @param func - The function. It receives an {@link ActionCtx} as its first argument, and a `Request` object 87 | * as its second. 88 | * @returns The wrapped endpoint function. Route a URL path to this function in `convex/http.js`. 89 | */ 90 | export const httpAction = httpActionGeneric; 91 | -------------------------------------------------------------------------------- /public/convex.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Convex 2 | 3 | This example demonstrates Convex presence. 4 | 5 | See [this post](https://stack.convex.dev/presence-with-convex) to learn more. 6 | 7 | # What is Convex? 8 | 9 | [Convex](https://convex.dev) is a hosted backend platform with a 10 | built-in database that lets you write your 11 | [database schema](https://docs.convex.dev/database/schemas) and 12 | [server functions](https://docs.convex.dev/functions) in 13 | [TypeScript](https://docs.convex.dev/typescript). Server-side database 14 | [queries](https://docs.convex.dev/functions/query-functions) automatically 15 | [cache](https://docs.convex.dev/functions/query-functions#caching--reactivity) and 16 | [subscribe](https://docs.convex.dev/client/react#reactivity) to data, powering a 17 | [realtime `useQuery` hook](https://docs.convex.dev/client/react#fetching-data) in our 18 | [React client](https://docs.convex.dev/client/react). There are also 19 | [Python](https://docs.convex.dev/client/python), 20 | [Rust](https://docs.convex.dev/client/rust), 21 | [ReactNative](https://docs.convex.dev/client/react-native), and 22 | [Node](https://docs.convex.dev/client/javascript) clients, as well as a straightforward 23 | [HTTP API](https://github.com/get-convex/convex-js/blob/main/src/browser/http_client.ts#L40). 24 | 25 | The database support 26 | [NoSQL-style documents](https://docs.convex.dev/database/document-storage) with 27 | [relationships](https://docs.convex.dev/database/document-ids) and 28 | [custom indexes](https://docs.convex.dev/database/indexes/) 29 | (including on fields in nested objects). 30 | 31 | The 32 | [`query`](https://docs.convex.dev/functions/query-functions) and 33 | [`mutation`](https://docs.convex.dev/functions/mutation-functions) server functions have transactional, 34 | low latency access to the database and leverage our 35 | [`v8` runtime](https://docs.convex.dev/functions/runtimes) with 36 | [determinism guardrails](https://docs.convex.dev/functions/runtimes#using-randomness-and-time-in-queries-and-mutations) 37 | to provide the strongest ACID guarantees on the market: 38 | immediate consistency, 39 | serializable isolation, and 40 | automatic conflict resolution via 41 | [optimistic multi-version concurrency control](https://docs.convex.dev/database/advanced/occ) (OCC / MVCC). 42 | 43 | The [`action` server functions](https://docs.convex.dev/functions/actions) have 44 | access to external APIs and enable other side-effects and non-determinism in 45 | either our 46 | [optimized `v8` runtime](https://docs.convex.dev/functions/runtimes) or a more 47 | [flexible `node` runtime](https://docs.convex.dev/functions/runtimes#nodejs-runtime). 48 | 49 | Functions can run in the background via 50 | [scheduling](https://docs.convex.dev/scheduling/scheduled-functions) and 51 | [cron jobs](https://docs.convex.dev/scheduling/cron-jobs). 52 | 53 | Development is cloud-first, with 54 | [hot reloads for server function](https://docs.convex.dev/cli#run-the-convex-dev-server) editing via the 55 | [CLI](https://docs.convex.dev/cli). There is a 56 | [dashbord UI](https://docs.convex.dev/dashboard) to 57 | [browse and edit data](https://docs.convex.dev/dashboard/deployments/data), 58 | [edit environment variables](https://docs.convex.dev/production/environment-variables), 59 | [view logs](https://docs.convex.dev/dashboard/deployments/logs), 60 | [run server functions](https://docs.convex.dev/dashboard/deployments/functions), and more. 61 | 62 | There are built-in features for 63 | [reactive pagination](https://docs.convex.dev/database/pagination), 64 | [file storage](https://docs.convex.dev/file-storage), 65 | [reactive search](https://docs.convex.dev/text-search), 66 | [https endpoints](https://docs.convex.dev/functions/http-actions) (for webhooks), 67 | [streaming import/export](https://docs.convex.dev/database/import-export/), and 68 | [runtime data validation](https://docs.convex.dev/database/schemas#validators) for 69 | [function arguments](https://docs.convex.dev/functions/args-validation) and 70 | [database data](https://docs.convex.dev/database/schemas#schema-validation). 71 | 72 | Everything scales automatically, and it’s [free to start](https://www.convex.dev/plans). 73 | -------------------------------------------------------------------------------- /convex/presence.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Functions related to reading & writing presence data. 3 | * 4 | * Note: this file does not currently implement authorization. 5 | * That is left as an exercise to the reader. Some suggestions for a production 6 | * app: 7 | * - Use Convex `auth` to authenticate users rather than passing up a "user" 8 | * - Check that the user is allowed to be in a given room. 9 | */ 10 | import { v } from 'convex/values'; 11 | import { query, mutation, internalMutation } from './_generated/server'; 12 | import { internal } from './_generated/api'; 13 | import { Doc } from './_generated/dataModel'; 14 | 15 | const LIST_LIMIT = 20; 16 | const MARK_AS_GONE_MS = 8_000; 17 | 18 | /** 19 | * Overwrites the presence data for a given user in a room. 20 | * 21 | * It will also set the "updated" timestamp to now, and create the presence 22 | * document if it doesn't exist yet. 23 | * 24 | * @param room - The location associated with the presence data. Examples: 25 | * page, chat channel, game instance. 26 | * @param user - The user associated with the presence data. 27 | */ 28 | export const update = mutation({ 29 | args: { room: v.string(), user: v.string(), data: v.any() }, 30 | handler: async (ctx, { room, user, data }) => { 31 | const existing = await ctx.db 32 | .query('presence') 33 | .withIndex('room_user', (q) => q.eq('room', room).eq('user', user)) 34 | .unique(); 35 | if (existing) { 36 | const patch: Partial> = { data }; 37 | if (existing.present === false) { 38 | patch.present = true; 39 | patch.latestJoin = Date.now(); 40 | } 41 | await ctx.db.patch(existing._id, { data }); 42 | } else { 43 | await ctx.db.insert('presence', { 44 | user, 45 | data, 46 | room, 47 | present: true, 48 | latestJoin: Date.now(), 49 | }); 50 | } 51 | }, 52 | }); 53 | 54 | /** 55 | * Updates the "updated" timestamp for a given user's presence in a room. 56 | * 57 | * @param room - The location associated with the presence data. Examples: 58 | * page, chat channel, game instance. 59 | * @param user - The user associated with the presence data. 60 | */ 61 | export const heartbeat = mutation({ 62 | args: { room: v.string(), user: v.string() }, 63 | handler: async (ctx, { room, user }) => { 64 | const existing = await ctx.db 65 | .query('presence_heartbeats') 66 | .withIndex('by_room_user', (q) => q.eq('room', room).eq('user', user)) 67 | .unique(); 68 | const markAsGone = await ctx.scheduler.runAfter( 69 | MARK_AS_GONE_MS, 70 | internal.presence.markAsGone, 71 | { room, user } 72 | ); 73 | if (existing) { 74 | const watchdog = await ctx.db.system.get(existing.markAsGone); 75 | if (watchdog && watchdog.state.kind === 'pending') { 76 | await ctx.scheduler.cancel(watchdog._id); 77 | } 78 | await ctx.db.patch(existing._id, { 79 | markAsGone, 80 | }); 81 | } else { 82 | await ctx.db.insert('presence_heartbeats', { 83 | user, 84 | room, 85 | markAsGone, 86 | }); 87 | } 88 | }, 89 | }); 90 | 91 | export const markAsGone = internalMutation({ 92 | args: { room: v.string(), user: v.string() }, 93 | handler: async (ctx, args) => { 94 | const presence = await ctx.db 95 | .query('presence') 96 | .withIndex('room_user', (q) => 97 | q.eq('room', args.room).eq('user', args.user) 98 | ) 99 | .unique(); 100 | if (!presence || presence.present === false) { 101 | return; 102 | } 103 | await ctx.db.patch(presence._id, { present: false }); 104 | }, 105 | }); 106 | 107 | /** 108 | * Lists the presence data for N users in a room, ordered by recent update. 109 | * 110 | * @param room - The location associated with the presence data. Examples: 111 | * page, chat channel, game instance. 112 | * @returns A list of presence objects, ordered by recent update, limited to 113 | * the most recent N. 114 | */ 115 | export const list = query({ 116 | args: { room: v.string() }, 117 | handler: async (ctx, { room }) => { 118 | const presence = await ctx.db 119 | .query('presence') 120 | .withIndex('room_present_join', (q) => 121 | q.eq('room', room).eq('present', true) 122 | ) 123 | .take(LIST_LIMIT); 124 | return presence.map( 125 | ({ _creationTime, latestJoin, user, data, present }) => ({ 126 | created: _creationTime, 127 | latestJoin, 128 | user, 129 | data, 130 | present, 131 | }) 132 | ); 133 | }, 134 | }); 135 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from 'next'; 2 | import dynamic from 'next/dynamic'; 3 | import Head from 'next/head'; 4 | import Image from 'next/image'; 5 | import { useState } from 'react'; 6 | import Facepile from '../components/Facepile'; 7 | import SharedCursors from '../components/SharedCursors'; 8 | import usePresence from '../hooks/usePresence'; 9 | import useTypingIndicator from '../hooks/useTypingIndicator'; 10 | 11 | const Emojis = 12 | '😀 😃 😄 😁 😆 😅 😂 🤣 🥲 🥹 😊 😇 🙂 🙃 😉 😌 😍 🥰 😘 😗 😙 😚 😋 😛 😝 😜 🤪 😎 🥸 🤩 🥳 😏 😳 🤔 🫢 🤭 🤫 😶 🫠 😮 🤤 😵‍💫 🥴 🤑 🤠'.split( 13 | ' ' 14 | ); 15 | 16 | const PresencePane = () => { 17 | const [userId] = useState(() => Math.floor(Math.random() * 10000)); 18 | const [location, setLocation] = useState('RoomA'); 19 | const [data, others, updatePresence] = usePresence( 20 | location, 21 | 'User' + userId, 22 | { 23 | text: '', 24 | emoji: Emojis[userId % Emojis.length], 25 | x: 0, 26 | y: 0, 27 | typing: false as boolean, 28 | } 29 | ); 30 | useTypingIndicator(data.text, updatePresence); 31 | const presentOthers = (others ?? []).filter((p) => p.present); 32 | return ( 33 |
34 | 43 |

44 | Simulating being on different pages 45 |

46 |
47 |

Facepile:

48 |
49 | 50 | 59 |
60 |

Shared cursors:

61 | 66 |

Shared text:

67 |
68 | 69 | {data.emoji + ': '} 70 | updatePresence({ text: e.target.value })} 77 | /> 78 | 79 |
    80 | {presentOthers 81 | .filter((p) => p.data.text) 82 | .sort((p1, p2) => p2.created - p1.created) 83 | .map((p) => ( 84 |
  • 85 |

    86 | {p.data.emoji + 87 | ': ' + 88 | p.data.text + 89 | (p.data.typing ? '...' : '')} 90 |

    91 |
  • 92 | ))} 93 |
94 |
95 |
96 |
97 | ); 98 | }; 99 | 100 | const PresencePaneNoSSR = dynamic(() => Promise.resolve(PresencePane), { 101 | ssr: false, 102 | }); 103 | 104 | const Home: NextPage = () => { 105 | return ( 106 |
107 | 108 | Presence with Convex 109 | 110 | 111 | 112 | 113 |
114 |

115 | Presence with{' '} 116 | 117 | Convex 118 | 119 |

120 |
121 | 122 | 123 |
124 |
125 | 126 | 140 |
141 | ); 142 | }; 143 | 144 | export default Home; 145 | -------------------------------------------------------------------------------- /convex/_generated/server.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * Generated utilities for implementing server-side Convex query and mutation functions. 4 | * 5 | * THIS CODE IS AUTOMATICALLY GENERATED. 6 | * 7 | * Generated by convex@1.11.0. 8 | * To regenerate, run `npx convex dev`. 9 | * @module 10 | */ 11 | 12 | import { 13 | ActionBuilder, 14 | HttpActionBuilder, 15 | MutationBuilder, 16 | QueryBuilder, 17 | GenericActionCtx, 18 | GenericMutationCtx, 19 | GenericQueryCtx, 20 | GenericDatabaseReader, 21 | GenericDatabaseWriter, 22 | } from "convex/server"; 23 | import type { DataModel } from "./dataModel.js"; 24 | 25 | /** 26 | * Define a query in this Convex app's public API. 27 | * 28 | * This function will be allowed to read your Convex database and will be accessible from the client. 29 | * 30 | * @param func - The query function. It receives a {@link QueryCtx} as its first argument. 31 | * @returns The wrapped query. Include this as an `export` to name it and make it accessible. 32 | */ 33 | export declare const query: QueryBuilder; 34 | 35 | /** 36 | * Define a query that is only accessible from other Convex functions (but not from the client). 37 | * 38 | * This function will be allowed to read from your Convex database. It will not be accessible from the client. 39 | * 40 | * @param func - The query function. It receives a {@link QueryCtx} as its first argument. 41 | * @returns The wrapped query. Include this as an `export` to name it and make it accessible. 42 | */ 43 | export declare const internalQuery: QueryBuilder; 44 | 45 | /** 46 | * Define a mutation in this Convex app's public API. 47 | * 48 | * This function will be allowed to modify your Convex database and will be accessible from the client. 49 | * 50 | * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. 51 | * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. 52 | */ 53 | export declare const mutation: MutationBuilder; 54 | 55 | /** 56 | * Define a mutation that is only accessible from other Convex functions (but not from the client). 57 | * 58 | * This function will be allowed to modify your Convex database. It will not be accessible from the client. 59 | * 60 | * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. 61 | * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. 62 | */ 63 | export declare const internalMutation: MutationBuilder; 64 | 65 | /** 66 | * Define an action in this Convex app's public API. 67 | * 68 | * An action is a function which can execute any JavaScript code, including non-deterministic 69 | * code and code with side-effects, like calling third-party services. 70 | * They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive. 71 | * They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}. 72 | * 73 | * @param func - The action. It receives an {@link ActionCtx} as its first argument. 74 | * @returns The wrapped action. Include this as an `export` to name it and make it accessible. 75 | */ 76 | export declare const action: ActionBuilder; 77 | 78 | /** 79 | * Define an action that is only accessible from other Convex functions (but not from the client). 80 | * 81 | * @param func - The function. It receives an {@link ActionCtx} as its first argument. 82 | * @returns The wrapped function. Include this as an `export` to name it and make it accessible. 83 | */ 84 | export declare const internalAction: ActionBuilder; 85 | 86 | /** 87 | * Define an HTTP action. 88 | * 89 | * This function will be used to respond to HTTP requests received by a Convex 90 | * deployment if the requests matches the path and method where this action 91 | * is routed. Be sure to route your action in `convex/http.js`. 92 | * 93 | * @param func - The function. It receives an {@link ActionCtx} as its first argument. 94 | * @returns The wrapped function. Import this function from `convex/http.js` and route it to hook it up. 95 | */ 96 | export declare const httpAction: HttpActionBuilder; 97 | 98 | /** 99 | * A set of services for use within Convex query functions. 100 | * 101 | * The query context is passed as the first argument to any Convex query 102 | * function run on the server. 103 | * 104 | * This differs from the {@link MutationCtx} because all of the services are 105 | * read-only. 106 | */ 107 | export type QueryCtx = GenericQueryCtx; 108 | 109 | /** 110 | * A set of services for use within Convex mutation functions. 111 | * 112 | * The mutation context is passed as the first argument to any Convex mutation 113 | * function run on the server. 114 | */ 115 | export type MutationCtx = GenericMutationCtx; 116 | 117 | /** 118 | * A set of services for use within Convex action functions. 119 | * 120 | * The action context is passed as the first argument to any Convex action 121 | * function run on the server. 122 | */ 123 | export type ActionCtx = GenericActionCtx; 124 | 125 | /** 126 | * An interface to read from the database within Convex query functions. 127 | * 128 | * The two entry points are {@link DatabaseReader.get}, which fetches a single 129 | * document by its {@link Id}, or {@link DatabaseReader.query}, which starts 130 | * building a query. 131 | */ 132 | export type DatabaseReader = GenericDatabaseReader; 133 | 134 | /** 135 | * An interface to read from and write to the database within Convex mutation 136 | * functions. 137 | * 138 | * Convex guarantees that all writes within a single mutation are 139 | * executed atomically, so you never have to worry about partial writes leaving 140 | * your data in an inconsistent state. See [the Convex Guide](https://docs.convex.dev/understanding/convex-fundamentals/functions#atomicity-and-optimistic-concurrency-control) 141 | * for the guarantees Convex provides your functions. 142 | */ 143 | export type DatabaseWriter = GenericDatabaseWriter; 144 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2024 Convex, Inc. 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | --------------------------------------------------------------------------------