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 | [](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 |
--------------------------------------------------------------------------------