├── example ├── src │ ├── vite-env.d.ts │ ├── App.tsx │ ├── main.tsx │ ├── App.css │ └── index.css ├── convex │ ├── tsconfig.json │ ├── schema.ts │ ├── convex.config.ts │ ├── example.ts │ ├── _generated │ │ ├── api.js │ │ ├── api.d.ts │ │ ├── dataModel.d.ts │ │ ├── server.js │ │ └── server.d.ts │ └── setup.test.ts ├── README.md ├── vite.config.ts ├── index.html └── tsconfig.json ├── .prettierrc.json ├── src ├── component │ ├── convex.config.ts │ ├── setup.test.ts │ ├── schema.ts │ ├── _generated │ │ ├── api.ts │ │ ├── dataModel.ts │ │ ├── component.ts │ │ └── server.ts │ ├── automerge.ts │ └── lib.ts ├── react │ └── index.ts ├── shared.ts ├── test.ts └── client │ └── index.ts ├── convex.json ├── CHANGELOG.md ├── vitest.config.js ├── tsconfig.build.json ├── tsconfig.test.json ├── .gitignore ├── CONTRIBUTING.md ├── renovate.json ├── tsconfig.json ├── .github └── workflows │ └── test.yml ├── README.md ├── node10stubs.mjs ├── eslint.config.js ├── package.json └── LICENSE /example/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "proseWrap": "always" 4 | } 5 | -------------------------------------------------------------------------------- /example/convex/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "include": ["."], 4 | "exclude": ["_generated"] 5 | } 6 | -------------------------------------------------------------------------------- /src/component/convex.config.ts: -------------------------------------------------------------------------------- 1 | import { defineComponent } from "convex/server"; 2 | 3 | export default defineComponent("automergeSync"); 4 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # Example app 2 | 3 | Components need an app that uses them in order to run codegen. An example app is 4 | also useful for testing and documentation. 5 | -------------------------------------------------------------------------------- /example/convex/schema.ts: -------------------------------------------------------------------------------- 1 | import { defineSchema } from "convex/server"; 2 | 3 | export default defineSchema({ 4 | // Any tables used by the example app go here. 5 | }); 6 | -------------------------------------------------------------------------------- /convex.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/convex/schemas/convex.schema.json", 3 | "functions": "example/convex", 4 | "codegen": { 5 | "legacyComponentApi": false 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /example/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | envDir: "../", 7 | plugins: [react()], 8 | }); 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.2.0 4 | 5 | - Adds /test and /\_generated/component.js entrypoints 6 | - Drops commonjs support 7 | - Improves source mapping for generated files 8 | - Changes to a statically generated component API 9 | -------------------------------------------------------------------------------- /example/convex/convex.config.ts: -------------------------------------------------------------------------------- 1 | import { defineApp } from "convex/server"; 2 | import automergeSync from "@convex-dev/automerge-sync/convex.config"; 3 | 4 | const app = defineApp(); 5 | app.use(automergeSync); 6 | 7 | export default app; 8 | -------------------------------------------------------------------------------- /vitest.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config"; 2 | 3 | export default defineConfig({ 4 | test: { 5 | environment: "edge-runtime", 6 | typecheck: { 7 | tsconfig: "./tsconfig.test.json", 8 | }, 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src/**/*"], 4 | "exclude": ["src/**/*.test.*", "./src/test.ts"], 5 | "compilerOptions": { 6 | "module": "ESNext", 7 | "moduleResolution": "Bundler" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/react/index.ts: -------------------------------------------------------------------------------- 1 | // This is where React components go. 2 | if (typeof window === "undefined") { 3 | throw new Error("this is frontend code, but it's running somewhere else!"); 4 | } 5 | 6 | export function subtract(a: number, b: number): number { 7 | return a - b; 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": [ 4 | "src/**/*.ts", 5 | "src/**/*.tsx", 6 | "example/src/**/*.ts", 7 | "example/src/**/*.tsx", 8 | "example/convex/**/*.ts" 9 | ], 10 | "exclude": ["node_modules", "dist", "**/_generated"] 11 | } 12 | -------------------------------------------------------------------------------- /example/convex/example.ts: -------------------------------------------------------------------------------- 1 | import { components } from "./_generated/api"; 2 | import { AutomergeSync } from "@convex-dev/automerge-sync"; 3 | 4 | const automergeSync = new AutomergeSync(components.automergeSync, { 5 | logLevel: "trace", 6 | }); 7 | 8 | export const { push, pull, compact } = automergeSync.syncApi(); 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea 3 | *.local 4 | *.log 5 | /.vscode/ 6 | /docs/.vitepress/cache 7 | dist 8 | dist-ssr 9 | explorations 10 | node_modules 11 | .eslintcache 12 | 13 | # this is a package-json-redirect stub dir, see https://github.com/andrewbranch/example-subpath-exports-ts-compat?tab=readme-ov-file 14 | react/package.json 15 | # npm pack output 16 | *.tgz 17 | *.tsbuildinfo 18 | -------------------------------------------------------------------------------- /src/component/setup.test.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { test } from "vitest"; 3 | import { convexTest } from "convex-test"; 4 | import schema from "./schema.js"; 5 | export const modules = import.meta.glob("./**/*.*s"); 6 | 7 | export function initConvexTest() { 8 | const t = convexTest(schema, modules); 9 | return t; 10 | } 11 | 12 | test("setup", () => {}); 13 | -------------------------------------------------------------------------------- /example/src/App.tsx: -------------------------------------------------------------------------------- 1 | import "./App.css"; 2 | 3 | function App() { 4 | return ( 5 | <> 6 |

Convex Automerge Sync Component Example

7 |
8 |

9 | See example/convex/example.ts for all the ways to use 10 | this component 11 |

12 |
13 | 14 | ); 15 | } 16 | 17 | export default App; 18 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/shared.ts: -------------------------------------------------------------------------------- 1 | import type { DocumentId } from "@automerge/automerge-repo"; 2 | import { v, type VString } from "convex/values"; 3 | 4 | export const vDocumentId = v.string() as VString; 5 | export const vDataType = v.union( 6 | v.literal("incremental"), 7 | v.literal("snapshot"), 8 | ); 9 | export const vLogLevel = v.union( 10 | v.literal("error"), 11 | v.literal("warn"), 12 | v.literal("info"), 13 | v.literal("debug"), 14 | v.literal("trace"), 15 | ); 16 | -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 5 | "skipLibCheck": false, 6 | "allowSyntheticDefaultImports": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "module": "ESNext", 10 | "moduleResolution": "Bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "react-jsx", 14 | "noEmit": true 15 | }, 16 | "include": ["./src", "vite.config.ts"] 17 | } 18 | -------------------------------------------------------------------------------- /example/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from "react"; 2 | import { createRoot } from "react-dom/client"; 3 | import { ConvexProvider, ConvexReactClient } from "convex/react"; 4 | import App from "./App.jsx"; 5 | import "./index.css"; 6 | 7 | const address = import.meta.env.VITE_CONVEX_URL; 8 | 9 | const convex = new ConvexReactClient(address); 10 | 11 | createRoot(document.getElementById("root")!).render( 12 | 13 | 14 | 15 | 16 | , 17 | ); 18 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Developing guide 2 | 3 | ## Running locally 4 | 5 | ```sh 6 | npm i 7 | npm run dev 8 | ``` 9 | 10 | ## Testing 11 | 12 | ```sh 13 | npm run clean 14 | npm run build 15 | npm run typecheck 16 | npm run lint 17 | npm run test 18 | ``` 19 | 20 | ## Deploying 21 | 22 | ### Building a one-off package 23 | 24 | ```sh 25 | npm run clean 26 | npm ci 27 | npm pack 28 | ``` 29 | 30 | ### Deploying a new version 31 | 32 | ```sh 33 | npm run release 34 | ``` 35 | 36 | or for alpha release: 37 | 38 | ```sh 39 | npm run alpha 40 | ``` 41 | -------------------------------------------------------------------------------- /example/convex/_generated/api.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * Generated `api` utility. 4 | * 5 | * THIS CODE IS AUTOMATICALLY GENERATED. 6 | * 7 | * To regenerate, run `npx convex dev`. 8 | * @module 9 | */ 10 | 11 | import { anyApi, componentsGeneric } from "convex/server"; 12 | 13 | /** 14 | * A utility for referencing Convex functions in your app's API. 15 | * 16 | * Usage: 17 | * ```js 18 | * const myFunctionReference = api.myModule.myFunction; 19 | * ``` 20 | */ 21 | export const api = anyApi; 22 | export const internal = anyApi; 23 | export const components = componentsGeneric(); 24 | -------------------------------------------------------------------------------- /example/convex/setup.test.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { test } from "vitest"; 3 | import { convexTest } from "convex-test"; 4 | import schema from "./schema.js"; 5 | import component from "@convex-dev/automerge-sync/test"; 6 | 7 | const modules = import.meta.glob("./**/*.*s"); 8 | // When users want to write tests that use your component, they need to 9 | // explicitly register it with its schema and modules. 10 | export function initConvexTest() { 11 | const t = convexTest(schema, modules); 12 | component.register(t); 13 | return t; 14 | } 15 | 16 | test("setup", () => {}); 17 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["config:best-practices"], 4 | "schedule": ["* 0-4 * * 1"], 5 | "timezone": "America/Los_Angeles", 6 | "prConcurrentLimit": 1, 7 | "packageRules": [ 8 | { 9 | "groupName": "Routine updates", 10 | "matchUpdateTypes": ["minor", "patch", "pin", "digest"], 11 | "automerge": true 12 | }, 13 | { 14 | "groupName": "Major updates", 15 | "matchUpdateTypes": ["major"], 16 | "automerge": false 17 | }, 18 | { 19 | "matchDepTypes": ["devDependencies"], 20 | "automerge": true 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /src/component/schema.ts: -------------------------------------------------------------------------------- 1 | import { defineTable, defineSchema } from "convex/server"; 2 | import { v } from "convex/values"; 3 | import { vDataType, vDocumentId } from "../shared.js"; 4 | 5 | export default defineSchema({ 6 | changes: defineTable({ 7 | documentId: vDocumentId, 8 | type: vDataType, 9 | heads: v.array(v.string()), 10 | data: v.bytes(), 11 | // Snapshots contain the entire document, for convenience of querying. 12 | contents: v.optional(v.any()), 13 | // For optionally storing raw change values, for debugging. 14 | debugDump: v.optional(v.any()), 15 | }) 16 | .index("by_type_key", ["documentId", "type", "heads"]) 17 | .index("by_insertion", ["documentId"]), 18 | }); 19 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import type { TestConvex } from "convex-test"; 3 | import type { GenericSchema, SchemaDefinition } from "convex/server"; 4 | import schema from "./component/schema.js"; 5 | const modules = import.meta.glob("./component/**/*.ts"); 6 | 7 | /** 8 | * Register the component with the test convex instance. 9 | * @param t - The test convex instance, e.g. from calling `convexTest`. 10 | * @param name - The name of the component, as registered in convex.config.ts. 11 | */ 12 | export function register( 13 | t: TestConvex>, 14 | name: string = "automergeSync", 15 | ) { 16 | t.registerComponent(name, schema, modules); 17 | } 18 | export default { register, schema, modules }; 19 | -------------------------------------------------------------------------------- /example/src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .logo { 9 | height: 6em; 10 | padding: 1.5em; 11 | will-change: filter; 12 | transition: filter 300ms; 13 | } 14 | .logo:hover { 15 | filter: drop-shadow(0 0 2em #646cffaa); 16 | } 17 | .logo.react:hover { 18 | filter: drop-shadow(0 0 2em #61dafbaa); 19 | } 20 | 21 | @keyframes logo-spin { 22 | from { 23 | transform: rotate(0deg); 24 | } 25 | to { 26 | transform: rotate(360deg); 27 | } 28 | } 29 | 30 | @media (prefers-reduced-motion: no-preference) { 31 | a:nth-of-type(2) .logo { 32 | animation: logo-spin infinite 20s linear; 33 | } 34 | } 35 | 36 | .card { 37 | padding: 2em; 38 | } 39 | 40 | .read-the-docs { 41 | color: #888; 42 | } 43 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "checkJs": true, 5 | "strict": true, 6 | 7 | "target": "ESNext", 8 | "lib": ["ES2021", "dom", "DOM.Iterable"], 9 | "jsx": "react-jsx", 10 | "forceConsistentCasingInFileNames": true, 11 | "allowSyntheticDefaultImports": true, 12 | "noErrorTruncation": true, 13 | // We enforce stricter module resolution for Node16 compatibility 14 | // But when building we use Bundler & ESNext for ESM 15 | "module": "Node16", 16 | "moduleResolution": "NodeNext", 17 | 18 | "composite": true, 19 | "isolatedModules": true, 20 | "declaration": true, 21 | "declarationMap": true, 22 | "sourceMap": true, 23 | "rootDir": "./src", 24 | "outDir": "./dist", 25 | "verbatimModuleSyntax": true, 26 | "skipLibCheck": true 27 | }, 28 | "include": ["./src/**/*"] 29 | } 30 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test and lint 2 | concurrency: 3 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} 4 | cancel-in-progress: true 5 | 6 | on: 7 | push: 8 | branches: [main] 9 | pull_request: 10 | branches: ["**"] 11 | 12 | jobs: 13 | check: 14 | name: Test and lint 15 | runs-on: ubuntu-latest 16 | timeout-minutes: 30 17 | 18 | steps: 19 | - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 20 | 21 | - name: Node setup 22 | uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5 23 | with: 24 | cache-dependency-path: package.json 25 | node-version: "20.x" 26 | cache: "npm" 27 | 28 | - name: Install and build 29 | run: | 30 | npm i 31 | npm run build 32 | - name: Publish package for testing branch 33 | run: npx pkg-pr-new publish || echo "Have you set up pkg-pr-new for this repo?" 34 | - name: Test 35 | run: | 36 | npm run test 37 | npm run typecheck 38 | npm run lint 39 | -------------------------------------------------------------------------------- /example/convex/_generated/api.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * Generated `api` utility. 4 | * 5 | * THIS CODE IS AUTOMATICALLY GENERATED. 6 | * 7 | * To regenerate, run `npx convex dev`. 8 | * @module 9 | */ 10 | 11 | import type * as example from "../example.js"; 12 | 13 | import type { 14 | ApiFromModules, 15 | FilterApi, 16 | FunctionReference, 17 | } from "convex/server"; 18 | 19 | declare const fullApi: ApiFromModules<{ 20 | example: typeof example; 21 | }>; 22 | 23 | /** 24 | * A utility for referencing Convex functions in your app's public API. 25 | * 26 | * Usage: 27 | * ```js 28 | * const myFunctionReference = api.myModule.myFunction; 29 | * ``` 30 | */ 31 | export declare const api: FilterApi< 32 | typeof fullApi, 33 | FunctionReference 34 | >; 35 | 36 | /** 37 | * A utility for referencing Convex functions in your app's internal API. 38 | * 39 | * Usage: 40 | * ```js 41 | * const myFunctionReference = internal.myModule.myFunction; 42 | * ``` 43 | */ 44 | export declare const internal: FilterApi< 45 | typeof fullApi, 46 | FunctionReference 47 | >; 48 | 49 | export declare const components: { 50 | automergeSync: import("@convex-dev/automerge-sync/_generated/component.js").ComponentApi<"automergeSync">; 51 | }; 52 | -------------------------------------------------------------------------------- /src/component/_generated/api.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * Generated `api` utility. 4 | * 5 | * THIS CODE IS AUTOMATICALLY GENERATED. 6 | * 7 | * To regenerate, run `npx convex dev`. 8 | * @module 9 | */ 10 | 11 | import type * as automerge from "../automerge.js"; 12 | import type * as lib from "../lib.js"; 13 | 14 | import type { 15 | ApiFromModules, 16 | FilterApi, 17 | FunctionReference, 18 | } from "convex/server"; 19 | import { anyApi, componentsGeneric } from "convex/server"; 20 | 21 | const fullApi: ApiFromModules<{ 22 | automerge: typeof automerge; 23 | lib: typeof lib; 24 | }> = anyApi as any; 25 | 26 | /** 27 | * A utility for referencing Convex functions in your app's public API. 28 | * 29 | * Usage: 30 | * ```js 31 | * const myFunctionReference = api.myModule.myFunction; 32 | * ``` 33 | */ 34 | export const api: FilterApi< 35 | typeof fullApi, 36 | FunctionReference 37 | > = anyApi as any; 38 | 39 | /** 40 | * A utility for referencing Convex functions in your app's internal API. 41 | * 42 | * Usage: 43 | * ```js 44 | * const myFunctionReference = internal.myModule.myFunction; 45 | * ``` 46 | */ 47 | export const internal: FilterApi< 48 | typeof fullApi, 49 | FunctionReference 50 | > = anyApi as any; 51 | 52 | export const components = componentsGeneric() as unknown as {}; 53 | -------------------------------------------------------------------------------- /example/src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-weight: 400; 5 | 6 | color-scheme: light dark; 7 | color: rgba(255, 255, 255, 0.87); 8 | background-color: #242424; 9 | 10 | font-synthesis: none; 11 | text-rendering: optimizeLegibility; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | } 15 | 16 | a { 17 | font-weight: 500; 18 | color: #646cff; 19 | text-decoration: inherit; 20 | } 21 | a:hover { 22 | color: #535bf2; 23 | } 24 | 25 | body { 26 | margin: 0; 27 | display: flex; 28 | place-items: center; 29 | min-width: 320px; 30 | min-height: 100vh; 31 | } 32 | 33 | h1 { 34 | font-size: 3.2em; 35 | line-height: 1.1; 36 | } 37 | 38 | button { 39 | border-radius: 8px; 40 | border: 1px solid transparent; 41 | padding: 0.6em 1.2em; 42 | font-size: 1em; 43 | font-weight: 500; 44 | font-family: inherit; 45 | background-color: #1a1a1a; 46 | cursor: pointer; 47 | transition: border-color 0.25s; 48 | } 49 | button:hover { 50 | border-color: #646cff; 51 | } 52 | button:focus, 53 | button:focus-visible { 54 | outline: 4px auto -webkit-focus-ring-color; 55 | } 56 | 57 | @media (prefers-color-scheme: light) { 58 | :root { 59 | color: #213547; 60 | background-color: #ffffff; 61 | } 62 | a:hover { 63 | color: #747bff; 64 | } 65 | button { 66 | background-color: #f9f9f9; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/component/automerge.ts: -------------------------------------------------------------------------------- 1 | import * as Automerge from "@automerge/automerge/slim/next"; 2 | // @ts-expect-error wasm is not a module 3 | import { automergeWasmBase64 } from "@automerge/automerge/automerge.wasm.base64.js"; 4 | import { query } from "./_generated/server.js"; 5 | import { v } from "convex/values"; 6 | import { mergeArrays } from "@automerge/automerge-repo/helpers/mergeArrays.js"; 7 | 8 | let loaded: Promise | undefined; 9 | async function loadWasm() { 10 | if (!loaded) { 11 | loaded = Automerge.initializeBase64Wasm(automergeWasmBase64 as string); 12 | } 13 | await loaded; 14 | return Automerge; 15 | } 16 | 17 | export const load = query({ 18 | args: { 19 | changes: v.array( 20 | v.object({ 21 | _id: v.id("changes"), 22 | data: v.bytes(), 23 | heads: v.array(v.string()), 24 | }), 25 | ), 26 | }, 27 | returns: v.object({ 28 | doc: v.record(v.string(), v.any()), 29 | heads: v.array(v.string()), 30 | missing: v.array(v.id("changes")), 31 | data: v.bytes(), 32 | }), 33 | handler: async (ctx, args) => { 34 | const A = await loadWasm(); 35 | const doc = A.loadIncremental( 36 | A.init(), 37 | mergeArrays(args.changes.map((c) => new Uint8Array(c.data))), 38 | ); 39 | const heads = A.getHeads(doc); 40 | const missing = args.changes 41 | .filter((c) => A.getMissingDeps(doc, c.heads).length > 0) 42 | .map((c) => c._id); 43 | const data = A.save(doc); 44 | return { 45 | doc, 46 | heads, 47 | missing, 48 | data: toArrayBuffer(data), 49 | }; 50 | }, 51 | }); 52 | 53 | function toArrayBuffer(data: Uint8Array): ArrayBuffer { 54 | return data.buffer.slice( 55 | data.byteOffset, 56 | data.byteOffset + data.byteLength, 57 | ) as ArrayBuffer; 58 | } 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Convex Automerge Sync Component 2 | 3 | [![npm version](https://badge.fury.io/js/@convex-dev%2Fautomerge-sync.svg)](https://badge.fury.io/js/@convex-dev%2Fautomerge-sync) 4 | 5 | **Note: Convex Components are currently in beta** 6 | 7 | 8 | 9 | - [ ] What is some compelling syntax as a hook? 10 | - [ ] Why should you use this component? 11 | - [ ] Links to Stack / other resources? 12 | 13 | Found a bug? Feature request? 14 | [File it here](https://github.com/get-convex/automerge-sync/issues). 15 | 16 | ## Pre-requisite: Convex 17 | 18 | You'll need an existing Convex project to use the component. Convex is a hosted 19 | backend platform, including a database, serverless functions, and a ton more you 20 | can learn about [here](https://docs.convex.dev/get-started). 21 | 22 | Run `npm create convex` or follow any of the 23 | [quickstarts](https://docs.convex.dev/home) to set one up. 24 | 25 | ## Installation 26 | 27 | Install the component package: 28 | 29 | ```ts 30 | npm install @convex-dev/automerge-sync 31 | ``` 32 | 33 | Create a `convex.config.ts` file in your app's `convex/` folder and install the 34 | component by calling `use`: 35 | 36 | ```ts 37 | // convex/convex.config.ts 38 | import { defineApp } from "convex/server"; 39 | import automergeSync from "@convex-dev/automerge-sync/convex.config.js"; 40 | 41 | const app = defineApp(); 42 | app.use(automergeSync); 43 | 44 | export default app; 45 | ``` 46 | 47 | ## Usage 48 | 49 | ```ts 50 | import { components } from "./_generated/api"; 51 | import { AutomergeSync } from "@convex-dev/automerge-sync"; 52 | 53 | const automergeSync = new AutomergeSync(components.automergeSync, { 54 | ...options, 55 | }); 56 | ``` 57 | 58 | See more example usage in [example.ts](./example/convex/example.ts). 59 | 60 | 61 | -------------------------------------------------------------------------------- /src/component/_generated/dataModel.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * Generated data model types. 4 | * 5 | * THIS CODE IS AUTOMATICALLY GENERATED. 6 | * 7 | * To regenerate, run `npx convex dev`. 8 | * @module 9 | */ 10 | 11 | import type { 12 | DataModelFromSchemaDefinition, 13 | DocumentByName, 14 | TableNamesInDataModel, 15 | SystemTableNames, 16 | } from "convex/server"; 17 | import type { GenericId } from "convex/values"; 18 | import schema from "../schema.js"; 19 | 20 | /** 21 | * The names of all of your Convex tables. 22 | */ 23 | export type TableNames = TableNamesInDataModel; 24 | 25 | /** 26 | * The type of a document stored in Convex. 27 | * 28 | * @typeParam TableName - A string literal type of the table name (like "users"). 29 | */ 30 | export type Doc = DocumentByName< 31 | DataModel, 32 | TableName 33 | >; 34 | 35 | /** 36 | * An identifier for a document in Convex. 37 | * 38 | * Convex documents are uniquely identified by their `Id`, which is accessible 39 | * on the `_id` field. To learn more, see [Document IDs](https://docs.convex.dev/using/document-ids). 40 | * 41 | * Documents can be loaded using `db.get(id)` in query and mutation functions. 42 | * 43 | * IDs are just strings at runtime, but this type can be used to distinguish them from other 44 | * strings when type checking. 45 | * 46 | * @typeParam TableName - A string literal type of the table name (like "users"). 47 | */ 48 | export type Id = 49 | GenericId; 50 | 51 | /** 52 | * A type describing your Convex data model. 53 | * 54 | * This type includes information about what tables you have, the type of 55 | * documents stored in those tables, and the indexes defined on them. 56 | * 57 | * This type is used to parameterize methods like `queryGeneric` and 58 | * `mutationGeneric` to make them type-safe. 59 | */ 60 | export type DataModel = DataModelFromSchemaDefinition; 61 | -------------------------------------------------------------------------------- /example/convex/_generated/dataModel.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * Generated data model types. 4 | * 5 | * THIS CODE IS AUTOMATICALLY GENERATED. 6 | * 7 | * To regenerate, run `npx convex dev`. 8 | * @module 9 | */ 10 | 11 | import type { 12 | DataModelFromSchemaDefinition, 13 | DocumentByName, 14 | TableNamesInDataModel, 15 | SystemTableNames, 16 | } from "convex/server"; 17 | import type { GenericId } from "convex/values"; 18 | import schema from "../schema.js"; 19 | 20 | /** 21 | * The names of all of your Convex tables. 22 | */ 23 | export type TableNames = TableNamesInDataModel; 24 | 25 | /** 26 | * The type of a document stored in Convex. 27 | * 28 | * @typeParam TableName - A string literal type of the table name (like "users"). 29 | */ 30 | export type Doc = DocumentByName< 31 | DataModel, 32 | TableName 33 | >; 34 | 35 | /** 36 | * An identifier for a document in Convex. 37 | * 38 | * Convex documents are uniquely identified by their `Id`, which is accessible 39 | * on the `_id` field. To learn more, see [Document IDs](https://docs.convex.dev/using/document-ids). 40 | * 41 | * Documents can be loaded using `db.get(id)` in query and mutation functions. 42 | * 43 | * IDs are just strings at runtime, but this type can be used to distinguish them from other 44 | * strings when type checking. 45 | * 46 | * @typeParam TableName - A string literal type of the table name (like "users"). 47 | */ 48 | export type Id = 49 | GenericId; 50 | 51 | /** 52 | * A type describing your Convex data model. 53 | * 54 | * This type includes information about what tables you have, the type of 55 | * documents stored in those tables, and the indexes defined on them. 56 | * 57 | * This type is used to parameterize methods like `queryGeneric` and 58 | * `mutationGeneric` to make them type-safe. 59 | */ 60 | export type DataModel = DataModelFromSchemaDefinition; 61 | -------------------------------------------------------------------------------- /node10stubs.mjs: -------------------------------------------------------------------------------- 1 | import fs from "fs/promises"; 2 | import path from "path"; 3 | 4 | async function findPackageJson(directory) { 5 | const packagePath = path.join(directory, "package.json"); 6 | try { 7 | await fs.access(packagePath); 8 | return packagePath; 9 | } catch (error) { 10 | const parentDir = path.dirname(directory); 11 | if (parentDir === directory) { 12 | throw new Error("package.json not found"); 13 | } 14 | return findPackageJson(parentDir); 15 | } 16 | } 17 | 18 | async function processSubPackages(packageJsonPath, exports, cleanup = false) { 19 | const baseDir = path.dirname(packageJsonPath); 20 | 21 | for (const [subDir, _] of Object.entries(exports)) { 22 | // package.json is already right where Node10 resolution would expect it. 23 | if (subDir.endsWith("package.json")) continue; 24 | // No need for Node10 resolution for component.config.ts 25 | if (subDir.endsWith("convex.config.js")) continue; 26 | // . just works with Node10 resolution 27 | if (subDir === ".") continue; 28 | console.log(subDir); 29 | 30 | const newDir = path.join(baseDir, subDir); 31 | const newPackageJsonPath = path.join(newDir, "package.json"); 32 | 33 | if (cleanup) { 34 | try { 35 | await fs.rm(newDir, { recursive: true, force: true }); 36 | } catch (error) { 37 | console.error(`Failed to remove ${newDir}:`, error.message); 38 | } 39 | } else { 40 | const newPackageJson = { 41 | main: `../dist/commonjs/${subDir}/index.js`, 42 | module: `../dist/esm/${subDir}/index.js`, 43 | types: `../dist/commonjs/${subDir}/index.d.ts`, 44 | }; 45 | 46 | await fs.mkdir(newDir, { recursive: true }); 47 | await fs.writeFile( 48 | newPackageJsonPath, 49 | JSON.stringify(newPackageJson, null, 2), 50 | ); 51 | } 52 | } 53 | } 54 | 55 | async function main() { 56 | try { 57 | const isCleanup = process.argv.includes("--cleanup"); 58 | const isAddFiles = process.argv.includes("--addFiles"); 59 | const packageJsonPath = await findPackageJson(process.cwd()); 60 | const packageJson = JSON.parse(await fs.readFile(packageJsonPath, "utf-8")); 61 | 62 | if (!packageJson.exports) { 63 | throw new Error("exports not found in package.json"); 64 | } 65 | 66 | if (isAddFiles) { 67 | return; 68 | } 69 | 70 | await processSubPackages(packageJsonPath, packageJson.exports, isCleanup); 71 | 72 | if (isCleanup) { 73 | console.log( 74 | "Node10 module resolution compatibility stub directories removed.", 75 | ); 76 | } else { 77 | console.log( 78 | "Node10 module resolution compatibility stub directories created", 79 | ); 80 | } 81 | } catch (error) { 82 | console.error("Error:", error.message); 83 | } 84 | } 85 | 86 | main(); 87 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import globals from "globals"; 2 | import pluginJs from "@eslint/js"; 3 | import tseslint from "typescript-eslint"; 4 | import reactHooks from "eslint-plugin-react-hooks"; 5 | import reactRefresh from "eslint-plugin-react-refresh"; 6 | 7 | export default [ 8 | { 9 | ignores: [ 10 | "dist/**", 11 | "eslint.config.js", 12 | "vitest.config.ts", 13 | "**/_generated/", 14 | "node10stubs.mjs", 15 | ], 16 | }, 17 | { 18 | files: ["src/**/*.{js,mjs,cjs,ts,tsx}", "example/**/*.{js,mjs,cjs,ts,tsx}"], 19 | languageOptions: { 20 | parser: tseslint.parser, 21 | parserOptions: { 22 | project: [ 23 | "./tsconfig.json", 24 | "./example/tsconfig.json", 25 | "./example/convex/tsconfig.json", 26 | ], 27 | tsconfigRootDir: import.meta.dirname, 28 | }, 29 | }, 30 | }, 31 | pluginJs.configs.recommended, 32 | ...tseslint.configs.recommended, 33 | // Convex code - Worker environment 34 | { 35 | files: ["src/**/*.{ts,tsx}", "example/convex/**/*.{ts,tsx}"], 36 | ignores: ["src/react/**"], 37 | languageOptions: { 38 | globals: globals.worker, 39 | }, 40 | rules: { 41 | "@typescript-eslint/no-floating-promises": "error", 42 | "@typescript-eslint/no-explicit-any": "off", 43 | "no-unused-vars": "off", 44 | "@typescript-eslint/no-unused-vars": [ 45 | "warn", 46 | { 47 | argsIgnorePattern: "^_", 48 | varsIgnorePattern: "^_", 49 | }, 50 | ], 51 | "@typescript-eslint/no-unused-expressions": [ 52 | "error", 53 | { 54 | allowShortCircuit: true, 55 | allowTernary: true, 56 | allowTaggedTemplates: true, 57 | }, 58 | ], 59 | }, 60 | }, 61 | // React app code - Browser environment 62 | { 63 | files: ["src/react/**/*.{ts,tsx}", "example/src/**/*.{ts,tsx}"], 64 | languageOptions: { 65 | ecmaVersion: 2020, 66 | globals: globals.browser, 67 | }, 68 | plugins: { 69 | "react-hooks": reactHooks, 70 | "react-refresh": reactRefresh, 71 | }, 72 | rules: { 73 | ...reactHooks.configs.recommended.rules, 74 | "react-refresh/only-export-components": [ 75 | "warn", 76 | { allowConstantExport: true }, 77 | ], 78 | "@typescript-eslint/no-explicit-any": "off", 79 | "no-unused-vars": "off", 80 | "@typescript-eslint/no-unused-vars": [ 81 | "warn", 82 | { 83 | argsIgnorePattern: "^_", 84 | varsIgnorePattern: "^_", 85 | }, 86 | ], 87 | }, 88 | }, 89 | // Example config files (vite.config.ts, etc.) - Node environment 90 | { 91 | files: ["example/vite.config.ts", "example/**/*.config.{js,ts}"], 92 | languageOptions: { 93 | globals: { 94 | ...globals.node, 95 | ...globals.browser, 96 | }, 97 | }, 98 | }, 99 | ]; 100 | -------------------------------------------------------------------------------- /src/component/_generated/component.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * Generated `ComponentApi` utility. 4 | * 5 | * THIS CODE IS AUTOMATICALLY GENERATED. 6 | * 7 | * To regenerate, run `npx convex dev`. 8 | * @module 9 | */ 10 | 11 | import type { FunctionReference } from "convex/server"; 12 | 13 | /** 14 | * A utility for referencing a Convex component's exposed API. 15 | * 16 | * Useful when expecting a parameter like `components.myComponent`. 17 | * Usage: 18 | * ```ts 19 | * async function myFunction(ctx: QueryCtx, component: ComponentApi) { 20 | * return ctx.runQuery(component.someFile.someQuery, { ...args }); 21 | * } 22 | * ``` 23 | */ 24 | export type ComponentApi = 25 | { 26 | automerge: { 27 | load: FunctionReference< 28 | "query", 29 | "internal", 30 | { 31 | changes: Array<{ 32 | _id: string; 33 | data: ArrayBuffer; 34 | heads: Array; 35 | }>; 36 | }, 37 | { 38 | data: ArrayBuffer; 39 | doc: Record; 40 | heads: Array; 41 | missing: Array; 42 | }, 43 | Name 44 | >; 45 | }; 46 | lib: { 47 | deleteDoc: FunctionReference< 48 | "mutation", 49 | "internal", 50 | { cursor?: string; documentId: string }, 51 | null, 52 | Name 53 | >; 54 | latestSnapshot: FunctionReference< 55 | "query", 56 | "internal", 57 | { documentId: string }, 58 | any, 59 | Name 60 | >; 61 | pull: FunctionReference< 62 | "query", 63 | "internal", 64 | { 65 | cursor: string | null; 66 | documentId: string; 67 | logLevel?: "error" | "warn" | "info" | "debug" | "trace"; 68 | numItems?: number; 69 | since: number; 70 | until?: number; 71 | }, 72 | { 73 | continueCursor: string; 74 | isDone: boolean; 75 | page: Array<{ 76 | _creationTime: number; 77 | _id: string; 78 | contents?: any; 79 | data: ArrayBuffer; 80 | debugDump?: any; 81 | documentId: string; 82 | heads: Array; 83 | type: "incremental" | "snapshot"; 84 | }>; 85 | pageStatus?: "SplitRecommended" | "SplitRequired" | null; 86 | splitCursor?: string | null; 87 | }, 88 | Name 89 | >; 90 | push: FunctionReference< 91 | "mutation", 92 | "internal", 93 | { 94 | contents?: any; 95 | data: ArrayBuffer; 96 | documentId: string; 97 | heads: Array; 98 | logLevel?: "error" | "warn" | "info" | "debug" | "trace"; 99 | replaces?: Array; 100 | type: "incremental" | "snapshot"; 101 | }, 102 | string, 103 | Name 104 | >; 105 | }; 106 | }; 107 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@convex-dev/automerge-sync", 3 | "description": "Sync Automerge documents with Convex.", 4 | "repository": "github:get-convex/automerge-sync", 5 | "homepage": "https://github.com/get-convex/automerge-sync#readme", 6 | "bugs": { 7 | "email": "support@convex.dev", 8 | "url": "https://github.com/get-convex/automerge-sync/issues" 9 | }, 10 | "version": "0.2.0", 11 | "license": "Apache-2.0", 12 | "keywords": [ 13 | "automerge", 14 | "crdt", 15 | "convex", 16 | "component" 17 | ], 18 | "type": "module", 19 | "scripts": { 20 | "dev": "run-p -r 'dev:*'", 21 | "dev:backend": "convex dev --typecheck-components", 22 | "dev:frontend": "cd example && vite --clearScreen false", 23 | "dev:build": "chokidar 'tsconfig*.json' 'src/**/*.ts' -i '**/*.test.ts' -c 'convex codegen --component-dir ./src/component && npm run build' --initial", 24 | "predev": "npm run dev:backend -- --until-success", 25 | "clean": "rm -rf dist *.tsbuildinfo", 26 | "build": "tsc --project ./tsconfig.build.json", 27 | "typecheck": "tsc --noEmit && tsc -p example && tsc -p example/convex", 28 | "lint": "eslint .", 29 | "all": "run-p -r 'dev:*' 'test:watch'", 30 | "test": "vitest run --typecheck", 31 | "test:watch": "vitest --typecheck --clearScreen false", 32 | "test:debug": "vitest --inspect-brk --no-file-parallelism", 33 | "test:coverage": "vitest run --coverage --coverage.reporter=text", 34 | "prepare": "npm run build", 35 | "alpha": "npm run clean && npm ci && run-p test lint typecheck && npm version prerelease --preid alpha && npm publish --tag alpha && git push --tags", 36 | "release": "npm run clean && npm ci && run-p test lint typecheck && npm version patch && npm publish && git push --tags", 37 | "version": "pbcopy <<<$npm_package_version; vim CHANGELOG.md && prettier -w CHANGELOG.md && git add CHANGELOG.md" 38 | }, 39 | "files": [ 40 | "dist", 41 | "src" 42 | ], 43 | "exports": { 44 | "./package.json": "./package.json", 45 | ".": { 46 | "types": "./dist/client/index.d.ts", 47 | "default": "./dist/client/index.js" 48 | }, 49 | "./react": { 50 | "types": "./dist/react/index.d.ts", 51 | "default": "./dist/react/index.js" 52 | }, 53 | "./test": "./src/test.ts", 54 | "./_generated/component.js": { 55 | "types": "./dist/component/_generated/component.d.ts" 56 | }, 57 | "./convex.config": { 58 | "types": "./dist/component/convex.config.d.ts", 59 | "default": "./dist/component/convex.config.js" 60 | }, 61 | "./convex.config.js": { 62 | "types": "./dist/component/convex.config.d.ts", 63 | "default": "./dist/component/convex.config.js" 64 | } 65 | }, 66 | "peerDependencies": { 67 | "@automerge/automerge": "^2.2.8", 68 | "@automerge/automerge-repo": "^1.2.1", 69 | "convex": "^1.24.8" 70 | }, 71 | "devDependencies": { 72 | "@automerge/automerge": "^2.2.8", 73 | "@automerge/automerge-repo": "^1.2.1", 74 | "@edge-runtime/vm": "5.0.0", 75 | "@eslint/eslintrc": "^3.3.1", 76 | "@eslint/js": "^9.38.0", 77 | "@types/node": "20.19.24", 78 | "@types/react": "^19.2.2", 79 | "@types/react-dom": "^19.2.2", 80 | "@vitejs/plugin-react": "^5.1.0", 81 | "chokidar-cli": "3.0.0", 82 | "convex": "1.29.0", 83 | "convex-test": "^0.0.33", 84 | "eslint": "^9.38.0", 85 | "eslint-plugin-react-hooks": "^7.0.1", 86 | "eslint-plugin-react-refresh": "^0.4.24", 87 | "globals": "^15.15.0", 88 | "npm-run-all2": "8.0.4", 89 | "prettier": "3.2.5", 90 | "react": "^18.3.1", 91 | "react-dom": "^18.3.1", 92 | "typescript": "5.8.3", 93 | "typescript-eslint": "^8.46.2", 94 | "vite": "^5.4.21", 95 | "vitest": "^3.2.4" 96 | }, 97 | "types": "./dist/client/index.d.ts", 98 | "module": "./dist/client/index.js" 99 | } 100 | -------------------------------------------------------------------------------- /example/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 | * To regenerate, run `npx convex dev`. 8 | * @module 9 | */ 10 | 11 | import { 12 | actionGeneric, 13 | httpActionGeneric, 14 | queryGeneric, 15 | mutationGeneric, 16 | internalActionGeneric, 17 | internalMutationGeneric, 18 | internalQueryGeneric, 19 | } from "convex/server"; 20 | 21 | /** 22 | * Define a query in this Convex app's public API. 23 | * 24 | * This function will be allowed to read your Convex database and will be accessible from the client. 25 | * 26 | * @param func - The query function. It receives a {@link QueryCtx} as its first argument. 27 | * @returns The wrapped query. Include this as an `export` to name it and make it accessible. 28 | */ 29 | export const query = queryGeneric; 30 | 31 | /** 32 | * Define a query that is only accessible from other Convex functions (but not from the client). 33 | * 34 | * This function will be allowed to read from your Convex database. It will not be accessible from the client. 35 | * 36 | * @param func - The query function. It receives a {@link QueryCtx} as its first argument. 37 | * @returns The wrapped query. Include this as an `export` to name it and make it accessible. 38 | */ 39 | export const internalQuery = internalQueryGeneric; 40 | 41 | /** 42 | * Define a mutation in this Convex app's public API. 43 | * 44 | * This function will be allowed to modify your Convex database and will be accessible from the client. 45 | * 46 | * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. 47 | * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. 48 | */ 49 | export const mutation = mutationGeneric; 50 | 51 | /** 52 | * Define a mutation that is only accessible from other Convex functions (but not from the client). 53 | * 54 | * This function will be allowed to modify your Convex database. It will not be accessible from the client. 55 | * 56 | * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. 57 | * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. 58 | */ 59 | export const internalMutation = internalMutationGeneric; 60 | 61 | /** 62 | * Define an action in this Convex app's public API. 63 | * 64 | * An action is a function which can execute any JavaScript code, including non-deterministic 65 | * code and code with side-effects, like calling third-party services. 66 | * They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive. 67 | * They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}. 68 | * 69 | * @param func - The action. It receives an {@link ActionCtx} as its first argument. 70 | * @returns The wrapped action. Include this as an `export` to name it and make it accessible. 71 | */ 72 | export const action = actionGeneric; 73 | 74 | /** 75 | * Define an action that is only accessible from other Convex functions (but not from the client). 76 | * 77 | * @param func - The function. It receives an {@link ActionCtx} as its first argument. 78 | * @returns The wrapped function. Include this as an `export` to name it and make it accessible. 79 | */ 80 | export const internalAction = internalActionGeneric; 81 | 82 | /** 83 | * Define an HTTP action. 84 | * 85 | * The wrapped function will be used to respond to HTTP requests received 86 | * by a Convex deployment if the requests matches the path and method where 87 | * this action is routed. Be sure to route your httpAction in `convex/http.js`. 88 | * 89 | * @param func - The function. It receives an {@link ActionCtx} as its first argument 90 | * and a Fetch API `Request` object as its second. 91 | * @returns The wrapped function. Import this function from `convex/http.js` and route it to hook it up. 92 | */ 93 | export const httpAction = httpActionGeneric; 94 | -------------------------------------------------------------------------------- /src/component/lib.ts: -------------------------------------------------------------------------------- 1 | import { v, type Validator, type Value } from "convex/values"; 2 | import { mutation, query } from "./_generated/server.js"; 3 | import { vDataType, vDocumentId, vLogLevel } from "../shared.js"; 4 | import { api } from "./_generated/api.js"; 5 | import schema from "./schema.js"; 6 | 7 | // TODO: set up logger 8 | 9 | export const push = mutation({ 10 | args: { 11 | documentId: vDocumentId, 12 | data: v.bytes(), 13 | type: vDataType, 14 | heads: v.array(v.string()), 15 | contents: v.optional(v.any()), 16 | logLevel: v.optional(vLogLevel), 17 | replaces: v.optional(v.array(v.id("changes"))), 18 | }, 19 | returns: v.id("changes"), 20 | handler: async (ctx, args) => { 21 | const existing = await ctx.db 22 | .query("changes") 23 | .withIndex("by_type_key", (q) => 24 | q 25 | .eq("documentId", args.documentId) 26 | .eq("type", args.type) 27 | .eq("heads", args.heads), 28 | ) 29 | .first(); 30 | const id = 31 | existing?._id ?? 32 | ctx.db.insert("changes", { 33 | documentId: args.documentId, 34 | type: args.type, 35 | heads: args.heads, 36 | data: args.data, 37 | contents: args.contents, 38 | }); 39 | if (args.replaces) { 40 | await Promise.all( 41 | args.replaces.map(async (id) => { 42 | const exists = await ctx.db.get(id); 43 | if (exists) { 44 | await ctx.db.delete(id); 45 | } 46 | }), 47 | ); 48 | } 49 | return id; 50 | }, 51 | }); 52 | 53 | const MINUTE = 60 * 1000; 54 | const RETENTION_BUFFER = 5 * MINUTE; 55 | 56 | export function vPaginationResult< 57 | T extends Validator, 58 | >(itemValidator: T) { 59 | return v.object({ 60 | page: v.array(itemValidator), 61 | continueCursor: v.string(), 62 | isDone: v.boolean(), 63 | splitCursor: v.optional(v.union(v.string(), v.null())), 64 | pageStatus: v.optional( 65 | v.union( 66 | v.literal("SplitRecommended"), 67 | v.literal("SplitRequired"), 68 | v.null(), 69 | ), 70 | ), 71 | }); 72 | } 73 | 74 | const vChange = v.object({ 75 | ...schema.tables.changes.validator.fields, 76 | _id: v.id("changes"), 77 | _creationTime: v.number(), 78 | }); 79 | 80 | export const pull = query({ 81 | args: { 82 | documentId: vDocumentId, 83 | since: v.number(), 84 | until: v.optional(v.number()), 85 | cursor: v.union(v.string(), v.null()), 86 | numItems: v.optional(v.number()), 87 | logLevel: v.optional(vLogLevel), 88 | }, 89 | returns: vPaginationResult(vChange), 90 | handler: async (ctx, args) => { 91 | const result = await ctx.db 92 | .query("changes") 93 | .withIndex("by_insertion", (q) => { 94 | const qq = q 95 | .eq("documentId", args.documentId) 96 | .gt("_creationTime", args.since); 97 | return args.until ? qq.lte("_creationTime", args.until) : qq; 98 | }) 99 | .paginate({ 100 | numItems: args.numItems ?? 10, 101 | cursor: args.cursor, 102 | }); 103 | 104 | // For the first page, also reach further back to avoid missing changes 105 | // inserted out of order. 106 | // This isn't part of the paginate call, since the cursors wouldn't 107 | // stay consistent if they're based on Date.now(). 108 | if (!args.cursor) { 109 | const retentionBuffer = await ctx.db 110 | .query("changes") 111 | .withIndex("by_insertion", (q) => 112 | q 113 | .eq("documentId", args.documentId) 114 | .gt("_creationTime", args.since - RETENTION_BUFFER) 115 | .lte("_creationTime", args.since), 116 | ) 117 | .collect(); 118 | result.page = retentionBuffer.concat(result.page); 119 | } 120 | return result; 121 | }, 122 | }); 123 | 124 | export const deleteDoc = mutation({ 125 | args: { documentId: vDocumentId, cursor: v.optional(v.string()) }, 126 | returns: v.null(), 127 | handler: async (ctx, args) => { 128 | const result = await ctx.db 129 | .query("changes") 130 | .withIndex("by_insertion", (q) => q.eq("documentId", args.documentId)) 131 | .paginate({ 132 | numItems: 1000, 133 | cursor: args.cursor ?? null, 134 | }); 135 | await Promise.all(result.page.map((c) => ctx.db.delete(c._id))); 136 | if (!result.isDone) { 137 | // TODO: logging 138 | await ctx.scheduler.runAfter(0, api.lib.deleteDoc, { 139 | documentId: args.documentId, 140 | cursor: result.continueCursor, 141 | }); 142 | } 143 | }, 144 | }); 145 | 146 | export const latestSnapshot = query({ 147 | args: { documentId: vDocumentId }, 148 | returns: v.any(), 149 | handler: async (ctx, args) => { 150 | const result = await ctx.db 151 | .query("changes") 152 | .withIndex("by_type_key", (q) => 153 | q.eq("documentId", args.documentId).eq("type", "snapshot"), 154 | ) 155 | .filter((q) => q.field("contents")) 156 | .order("desc") 157 | .first(); 158 | return result?.contents; 159 | }, 160 | }); 161 | -------------------------------------------------------------------------------- /example/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 | * To regenerate, run `npx convex dev`. 8 | * @module 9 | */ 10 | 11 | import { 12 | ActionBuilder, 13 | HttpActionBuilder, 14 | MutationBuilder, 15 | QueryBuilder, 16 | GenericActionCtx, 17 | GenericMutationCtx, 18 | GenericQueryCtx, 19 | GenericDatabaseReader, 20 | GenericDatabaseWriter, 21 | } from "convex/server"; 22 | import type { DataModel } from "./dataModel.js"; 23 | 24 | /** 25 | * Define a query in this Convex app's public API. 26 | * 27 | * This function will be allowed to read your Convex database and will be accessible from the client. 28 | * 29 | * @param func - The query function. It receives a {@link QueryCtx} as its first argument. 30 | * @returns The wrapped query. Include this as an `export` to name it and make it accessible. 31 | */ 32 | export declare const query: QueryBuilder; 33 | 34 | /** 35 | * Define a query that is only accessible from other Convex functions (but not from the client). 36 | * 37 | * This function will be allowed to read from your Convex database. It will not be accessible from the client. 38 | * 39 | * @param func - The query function. It receives a {@link QueryCtx} as its first argument. 40 | * @returns The wrapped query. Include this as an `export` to name it and make it accessible. 41 | */ 42 | export declare const internalQuery: QueryBuilder; 43 | 44 | /** 45 | * Define a mutation in this Convex app's public API. 46 | * 47 | * This function will be allowed to modify your Convex database and will be accessible from the client. 48 | * 49 | * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. 50 | * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. 51 | */ 52 | export declare const mutation: MutationBuilder; 53 | 54 | /** 55 | * Define a mutation that is only accessible from other Convex functions (but not from the client). 56 | * 57 | * This function will be allowed to modify your Convex database. It will not be accessible from the client. 58 | * 59 | * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. 60 | * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. 61 | */ 62 | export declare const internalMutation: MutationBuilder; 63 | 64 | /** 65 | * Define an action in this Convex app's public API. 66 | * 67 | * An action is a function which can execute any JavaScript code, including non-deterministic 68 | * code and code with side-effects, like calling third-party services. 69 | * They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive. 70 | * They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}. 71 | * 72 | * @param func - The action. It receives an {@link ActionCtx} as its first argument. 73 | * @returns The wrapped action. Include this as an `export` to name it and make it accessible. 74 | */ 75 | export declare const action: ActionBuilder; 76 | 77 | /** 78 | * Define an action that is only accessible from other Convex functions (but not from the client). 79 | * 80 | * @param func - The function. It receives an {@link ActionCtx} as its first argument. 81 | * @returns The wrapped function. Include this as an `export` to name it and make it accessible. 82 | */ 83 | export declare const internalAction: ActionBuilder; 84 | 85 | /** 86 | * Define an HTTP action. 87 | * 88 | * The wrapped function will be used to respond to HTTP requests received 89 | * by a Convex deployment if the requests matches the path and method where 90 | * this action is routed. Be sure to route your httpAction in `convex/http.js`. 91 | * 92 | * @param func - The function. It receives an {@link ActionCtx} as its first argument 93 | * and a Fetch API `Request` object as its second. 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 | -------------------------------------------------------------------------------- /src/component/_generated/server.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 | * To regenerate, run `npx convex dev`. 8 | * @module 9 | */ 10 | 11 | import type { 12 | ActionBuilder, 13 | HttpActionBuilder, 14 | MutationBuilder, 15 | QueryBuilder, 16 | GenericActionCtx, 17 | GenericMutationCtx, 18 | GenericQueryCtx, 19 | GenericDatabaseReader, 20 | GenericDatabaseWriter, 21 | } from "convex/server"; 22 | import { 23 | actionGeneric, 24 | httpActionGeneric, 25 | queryGeneric, 26 | mutationGeneric, 27 | internalActionGeneric, 28 | internalMutationGeneric, 29 | internalQueryGeneric, 30 | } from "convex/server"; 31 | import type { DataModel } from "./dataModel.js"; 32 | 33 | /** 34 | * Define a query in this Convex app's public API. 35 | * 36 | * This function will be allowed to read your Convex database and will be accessible from the client. 37 | * 38 | * @param func - The query function. It receives a {@link QueryCtx} as its first argument. 39 | * @returns The wrapped query. Include this as an `export` to name it and make it accessible. 40 | */ 41 | export const query: QueryBuilder = queryGeneric; 42 | 43 | /** 44 | * Define a query that is only accessible from other Convex functions (but not from the client). 45 | * 46 | * This function will be allowed to read from your Convex database. It will not be accessible from the client. 47 | * 48 | * @param func - The query function. It receives a {@link QueryCtx} as its first argument. 49 | * @returns The wrapped query. Include this as an `export` to name it and make it accessible. 50 | */ 51 | export const internalQuery: QueryBuilder = 52 | internalQueryGeneric; 53 | 54 | /** 55 | * Define a mutation in this Convex app's public API. 56 | * 57 | * This function will be allowed to modify your Convex database and will be accessible from the client. 58 | * 59 | * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. 60 | * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. 61 | */ 62 | export const mutation: MutationBuilder = mutationGeneric; 63 | 64 | /** 65 | * Define a mutation that is only accessible from other Convex functions (but not from the client). 66 | * 67 | * This function will be allowed to modify your Convex database. It will not be accessible from the client. 68 | * 69 | * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. 70 | * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. 71 | */ 72 | export const internalMutation: MutationBuilder = 73 | internalMutationGeneric; 74 | 75 | /** 76 | * Define an action in this Convex app's public API. 77 | * 78 | * An action is a function which can execute any JavaScript code, including non-deterministic 79 | * code and code with side-effects, like calling third-party services. 80 | * They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive. 81 | * They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}. 82 | * 83 | * @param func - The action. It receives an {@link ActionCtx} as its first argument. 84 | * @returns The wrapped action. Include this as an `export` to name it and make it accessible. 85 | */ 86 | export const action: ActionBuilder = actionGeneric; 87 | 88 | /** 89 | * Define an action that is only accessible from other Convex functions (but not from the client). 90 | * 91 | * @param func - The function. It receives an {@link ActionCtx} as its first argument. 92 | * @returns The wrapped function. Include this as an `export` to name it and make it accessible. 93 | */ 94 | export const internalAction: ActionBuilder = 95 | internalActionGeneric; 96 | 97 | /** 98 | * Define an HTTP action. 99 | * 100 | * The wrapped function will be used to respond to HTTP requests received 101 | * by a Convex deployment if the requests matches the path and method where 102 | * this action is routed. Be sure to route your httpAction in `convex/http.js`. 103 | * 104 | * @param func - The function. It receives an {@link ActionCtx} as its first argument 105 | * and a Fetch API `Request` object as its second. 106 | * @returns The wrapped function. Import this function from `convex/http.js` and route it to hook it up. 107 | */ 108 | export const httpAction: HttpActionBuilder = httpActionGeneric; 109 | 110 | type GenericCtx = 111 | | GenericActionCtx 112 | | GenericMutationCtx 113 | | GenericQueryCtx; 114 | 115 | /** 116 | * A set of services for use within Convex query functions. 117 | * 118 | * The query context is passed as the first argument to any Convex query 119 | * function run on the server. 120 | * 121 | * If you're using code generation, use the `QueryCtx` type in `convex/_generated/server.d.ts` instead. 122 | */ 123 | export type QueryCtx = GenericQueryCtx; 124 | 125 | /** 126 | * A set of services for use within Convex mutation functions. 127 | * 128 | * The mutation context is passed as the first argument to any Convex mutation 129 | * function run on the server. 130 | * 131 | * If you're using code generation, use the `MutationCtx` type in `convex/_generated/server.d.ts` instead. 132 | */ 133 | export type MutationCtx = GenericMutationCtx; 134 | 135 | /** 136 | * A set of services for use within Convex action functions. 137 | * 138 | * The action context is passed as the first argument to any Convex action 139 | * function run on the server. 140 | */ 141 | export type ActionCtx = GenericActionCtx; 142 | 143 | /** 144 | * An interface to read from the database within Convex query functions. 145 | * 146 | * The two entry points are {@link DatabaseReader.get}, which fetches a single 147 | * document by its {@link Id}, or {@link DatabaseReader.query}, which starts 148 | * building a query. 149 | */ 150 | export type DatabaseReader = GenericDatabaseReader; 151 | 152 | /** 153 | * An interface to read from and write to the database within Convex mutation 154 | * functions. 155 | * 156 | * Convex guarantees that all writes within a single mutation are 157 | * executed atomically, so you never have to worry about partial writes leaving 158 | * your data in an inconsistent state. See [the Convex Guide](https://docs.convex.dev/understanding/convex-fundamentals/functions#atomicity-and-optimistic-concurrency-control) 159 | * for the guarantees Convex provides your functions. 160 | */ 161 | export type DatabaseWriter = GenericDatabaseWriter; 162 | -------------------------------------------------------------------------------- /src/client/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type GenericDataModel, 3 | type GenericMutationCtx, 4 | type GenericQueryCtx, 5 | mutationGeneric, 6 | queryGeneric, 7 | } from "convex/server"; 8 | import { ConvexError, type Infer, v } from "convex/values"; 9 | import type { DocumentId } from "@automerge/automerge-repo"; 10 | import { vDataType, vDocumentId, vLogLevel } from "../shared.js"; 11 | import type { ComponentApi } from "../component/_generated/component.js"; 12 | 13 | export const PERMISSION_ERROR = "permission_denied" as const; 14 | 15 | export class AutomergeSync { 16 | constructor( 17 | public component: ComponentApi, 18 | private opts?: { 19 | // TODO: allow overriding snapshot function 20 | logLevel?: "error" | "warn" | "info" | "debug" | "trace"; 21 | }, 22 | ) {} 23 | async load(ctx: RunQueryCtx, documentId: DocumentId) { 24 | const changes = await this.pull(ctx, { 25 | documentId: documentId, 26 | since: 0, 27 | cursor: null, 28 | numItems: 1000, 29 | }); 30 | const { doc, heads } = await ctx.runQuery(this.component.automerge.load, { 31 | changes: changes.page.map((c) => ({ 32 | _id: c._id, 33 | data: c.data, 34 | heads: c.heads, 35 | })), 36 | }); 37 | return { doc, heads, isCurrent: changes.isDone }; 38 | } 39 | async latestSnapshot(ctx: RunQueryCtx, documentId: DocumentId) { 40 | return ctx.runQuery(this.component.lib.latestSnapshot, { documentId }); 41 | } 42 | 43 | async delete(ctx: RunQueryCtx & RunMutationCtx, documentId: DocumentId) { 44 | await ctx.runMutation(this.component.lib.deleteDoc, { documentId }); 45 | } 46 | 47 | pullArgs = v.object({ 48 | documentId: vDocumentId, 49 | since: v.number(), 50 | until: v.optional(v.number()), 51 | cursor: v.union(v.string(), v.null()), 52 | numItems: v.optional(v.number()), 53 | logLevel: v.optional(vLogLevel), 54 | }); 55 | async pull(ctx: RunQueryCtx, args: Infer) { 56 | return ctx.runQuery(this.component.lib.pull, args); 57 | } 58 | 59 | pushArgs = v.object({ 60 | documentId: vDocumentId, 61 | data: v.bytes(), 62 | type: vDataType, 63 | heads: v.array(v.string()), 64 | contents: v.optional(v.any()), 65 | logLevel: v.optional(vLogLevel), 66 | replaces: v.optional(v.array(v.string())), 67 | }); 68 | async push(ctx: RunMutationCtx, args: Infer) { 69 | return ctx.runMutation(this.component.lib.push, args); 70 | } 71 | 72 | compactArgs = v.object({ 73 | documentId: vDocumentId, 74 | cursor: v.optional(v.string()), 75 | until: v.number(), 76 | }); 77 | async compact( 78 | ctx: RunQueryCtx & RunMutationCtx, 79 | args: Infer, 80 | ) { 81 | const changes = await this.pull(ctx, { 82 | documentId: args.documentId, 83 | since: 0, 84 | cursor: args.cursor ?? null, 85 | numItems: 1000, 86 | until: args.until, 87 | }); 88 | if (changes.page.length === 0) { 89 | return { 90 | doc: null, 91 | isDone: true, 92 | continueCursor: null, 93 | }; 94 | } 95 | const { doc, heads, missing, data } = await ctx.runQuery( 96 | this.component.automerge.load, 97 | { 98 | changes: changes.page.map((c) => ({ 99 | _id: c._id, 100 | data: c.data, 101 | heads: c.heads, 102 | })), 103 | }, 104 | ); 105 | const missingIds = new Set(missing); 106 | if (missingIds.size > 0) { 107 | // TODO: warning! 108 | } 109 | if (changes.page.length > 1) { 110 | await this.push(ctx, { 111 | documentId: args.documentId, 112 | data, 113 | type: "snapshot", 114 | heads, 115 | contents: doc, 116 | replaces: changes.page 117 | .filter((c) => !missingIds.has(c._id)) 118 | .map((c) => c._id), 119 | }); 120 | } else { 121 | // TODO: log 122 | } 123 | return { 124 | doc, 125 | isDone: changes.isDone, 126 | continueCursor: changes.continueCursor, 127 | }; 128 | } 129 | 130 | /** 131 | * For easy re-exporting. 132 | * Apps can do 133 | * ```ts 134 | * export const { add, count } = automergeSync.api(); 135 | * ``` 136 | */ 137 | syncApi(callbacks?: { 138 | canRead: ( 139 | ctx: GenericQueryCtx, 140 | documentId: DocumentId, 141 | ) => boolean | Promise; 142 | canChange: ( 143 | ctx: GenericMutationCtx, 144 | documentId: DocumentId, 145 | ) => boolean | Promise; 146 | onSnapshot?: ( 147 | ctx: GenericMutationCtx, 148 | documentId: DocumentId, 149 | doc: T, 150 | isCurrent: boolean, 151 | ) => void | Promise; 152 | }) { 153 | return { 154 | pull: queryGeneric({ 155 | args: this.pullArgs, 156 | handler: async (ctx, args) => { 157 | if ( 158 | callbacks?.canRead && 159 | !(await callbacks.canRead(ctx, args.documentId)) 160 | ) { 161 | throw new ConvexError(PERMISSION_ERROR); 162 | } 163 | return this.pull(ctx, args); 164 | }, 165 | }), 166 | push: mutationGeneric({ 167 | args: this.pushArgs, 168 | handler: async (ctx, args) => { 169 | if ( 170 | callbacks?.canChange && 171 | !(await callbacks.canChange(ctx, args.documentId)) 172 | ) { 173 | throw new ConvexError(PERMISSION_ERROR); 174 | } 175 | return this.push(ctx, args); 176 | }, 177 | }), 178 | compact: mutationGeneric({ 179 | args: v.object({ 180 | documentId: vDocumentId, 181 | cursor: v.optional(v.string()), 182 | until: v.number(), 183 | }), 184 | handler: async (ctx, args) => { 185 | if ( 186 | callbacks?.canChange && 187 | !(await callbacks.canChange(ctx, args.documentId)) 188 | ) { 189 | throw new ConvexError(PERMISSION_ERROR); 190 | } 191 | const { doc, isDone, continueCursor } = await this.compact(ctx, args); 192 | if (callbacks?.onSnapshot) { 193 | await callbacks.onSnapshot(ctx, args.documentId, doc as T, isDone); 194 | } 195 | return { 196 | doc, 197 | isDone, 198 | continueCursor, 199 | }; 200 | }, 201 | }), 202 | }; 203 | } 204 | } 205 | 206 | /* Type utils follow */ 207 | 208 | type RunQueryCtx = { 209 | runQuery: GenericQueryCtx["runQuery"]; 210 | }; 211 | type RunMutationCtx = { 212 | runMutation: GenericMutationCtx["runMutation"]; 213 | }; 214 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------