From 16493840446f8bd9ce944782e86e153c136b7f5f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Feb 2026 21:07:15 +0000 Subject: [PATCH 1/3] Initial plan From dfea6e77e42e556a24735e41421a0344f7b1b46f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Feb 2026 21:11:42 +0000 Subject: [PATCH 2/3] Fix PR review comments: context providers, exports, and examples Co-authored-by: miccy <9729864+miccy@users.noreply.github.com> --- .../playgrounds/full/EvoluFullExample.tsx | 1854 ++++++++--------- .../minimal/EvoluMinimalExample.tsx | 640 +++--- examples/react-expo/app/index.tsx | 1767 ++++++++-------- examples/react-expo/package.json | 2 +- packages/common/src/local-first/Evolu.ts | 7 +- .../src/exports/expo-op-sqlite.ts | 30 +- packages/react/src/EvoluProvider.tsx | 15 + packages/react/src/Task.tsx | 6 +- packages/react/src/createUseEvolu.ts | 17 + packages/react/src/index.ts | 2 + .../react/src/local-first/EvoluContext.tsx | 20 +- packages/vue/src/index.ts | 1 + packages/vue/src/useEvoluError.ts | 17 + 13 files changed, 2148 insertions(+), 2230 deletions(-) create mode 100644 packages/react/src/EvoluProvider.tsx create mode 100644 packages/react/src/createUseEvolu.ts create mode 100644 packages/vue/src/useEvoluError.ts diff --git a/apps/web/src/app/(playgrounds)/playgrounds/full/EvoluFullExample.tsx b/apps/web/src/app/(playgrounds)/playgrounds/full/EvoluFullExample.tsx index e78636a39..edd8ec328 100644 --- a/apps/web/src/app/(playgrounds)/playgrounds/full/EvoluFullExample.tsx +++ b/apps/web/src/app/(playgrounds)/playgrounds/full/EvoluFullExample.tsx @@ -1,929 +1,929 @@ "use client"; -import type { FC } from "react"; - -export const EvoluFullExample: FC = () =>
TODO
; - -// import { -// booleanToSqliteBoolean, -// createEvolu, -// createFormatTypeError, -// createObjectURL, -// createQueryBuilder, -// FiniteNumber, -// id, -// idToIdBytes, -// json, -// kysely, -// maxLength, -// Mnemonic, -// NonEmptyString, -// NonEmptyTrimmedString100, -// nullOr, -// object, -// SimpleName, -// SqliteBoolean, -// sqliteFalse, -// sqliteTrue, -// timestampBytesToTimestamp, -// type MaxLengthError, -// type MinLengthError, -// } from "@evolu/common"; -// import { timestampToDateIso } from "@evolu/common/local-first"; -// import { -// createUseEvolu, -// EvoluProvider, -// useQueries, -// useQuery, -// } from "@evolu/react"; -// import { createEvoluDeps } from "@evolu/react-web"; -// import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react"; -// import { -// IconChecklist, -// IconEdit, -// IconHistory, -// IconRestore, -// IconTrash, -// } from "@tabler/icons-react"; -// import clsx from "clsx"; -// import { -// startTransition, -// Suspense, -// use, -// useState, -// type FC, -// type KeyboardEvent, -// } from "react"; - -// // TODO: Epochs and sharing. - -// const ProjectId = id("Project"); -// type ProjectId = typeof ProjectId.Type; - -// const TodoId = id("Todo"); -// type TodoId = typeof TodoId.Type; - -// // A custom branded Type. -// const NonEmptyString50 = maxLength(50)(NonEmptyString); -// // string & Brand<"MinLength1"> & Brand<"MaxLength50"> -// type NonEmptyString50 = typeof NonEmptyString50.Type; - -// // SQLite supports JSON values. -// // Use JSON for semi-structured data like API responses, external integrations, -// // or when the schema varies by use case. -// // Let's create an object to demonstrate it. -// const Foo = object({ -// foo: NonEmptyString50, -// // Did you know that JSON.stringify converts NaN (a number) into null? -// // To prevent this, use FiniteNumber. -// bar: FiniteNumber, -// }); -// type Foo = typeof Foo.Type; - -// // SQLite stores JSON values as strings. Evolu provides a convenient `json` -// // Type Factory for type-safe JSON serialization and parsing. -// const [FooJson, fooToFooJson, fooJsonToFoo] = json(Foo, "FooJson"); -// // string & Brand<"FooJson"> -// type FooJson = typeof FooJson.Type; - -// const Schema = { -// project: { -// id: ProjectId, -// name: NonEmptyTrimmedString100, -// fooJson: FooJson, -// }, -// todo: { -// id: TodoId, -// title: NonEmptyTrimmedString100, -// isCompleted: nullOr(SqliteBoolean), -// projectId: nullOr(ProjectId), -// }, -// }; - -// const createQuery = createQueryBuilder(Schema); - -// const deps = createEvoluDeps(); - -// const evolu = createEvolu(deps)(Schema, { -// name: SimpleName.orThrow("full-example"), - -// // reloadUrl: "/playgrounds/full", - -// ...(process.env.NODE_ENV === "development" && { -// transports: [{ type: "WebSocket", url: "ws://localhost:4000" }], - -// // Empty transports for local-only instance. -// // transports: [], -// }), - -// // https://www.evolu.dev/docs/indexes -// indexes: (create) => [ -// create("todoCreatedAt").on("todo").column("createdAt"), -// create("projectCreatedAt").on("project").column("createdAt"), -// create("todoProjectId").on("todo").column("projectId"), -// ], - -// // enableLogging: false, -// }); - -// const useEvolu = createUseEvolu(evolu); - -// evolu.subscribeError(() => { -// const error = evolu.getError(); -// if (!error) return; - -// alert("🚨 Evolu error occurred! Check the console."); -// // eslint-disable-next-line no-console -// console.error(error); -// }); - -// export const EvoluFullExample: FC = () => ( -//
-//
-// -// -// -// -// -//
-//
-// ); - -// const App: FC = () => { -// const [activeTab, setActiveTab] = useState< -// "home" | "projects" | "account" | "trash" -// >("home"); - -// const createHandleTabClick = (tab: typeof activeTab) => () => { -// // startTransition prevents UI flickers when switching tabs by keeping -// // the current view visible while Suspense prepares the next one -// // Test: Remove startTransition, add a todo, delete it, click to Trash. -// // You will see a visible blink without startTransition. -// startTransition(() => { -// setActiveTab(tab); -// }); -// }; - -// return ( -//
-//
-//
-// -// -// -// -//
-//
- -// {activeTab === "home" && } -// {activeTab === "projects" && } -// {activeTab === "account" && } -// {activeTab === "trash" && } -//
-// ); -// }; - -// const projectsWithTodosQuery = createQuery( -// (db) => -// db -// .selectFrom("project") -// .select(["id", "name"]) -// // https://kysely.dev/docs/recipes/relations -// .select((eb) => [ -// kysely -// .jsonArrayFrom( -// eb -// .selectFrom("todo") -// .select([ -// "todo.id", -// "todo.title", -// "todo.isCompleted", -// "todo.projectId", -// ]) -// .whereRef("todo.projectId", "=", "project.id") -// .where("todo.isDeleted", "is not", sqliteTrue) -// .where("todo.title", "is not", null) -// .$narrowType<{ title: kysely.NotNull }>() -// .orderBy("createdAt"), -// ) -// .as("todos"), -// ]) -// .where("project.isDeleted", "is not", sqliteTrue) -// .where("name", "is not", null) -// .$narrowType<{ name: kysely.NotNull }>() -// .orderBy("createdAt"), -// { -// // Log how long each query execution takes -// logQueryExecutionTime: false, - -// // Log the SQLite query execution plan for optimization analysis -// logExplainQueryPlan: false, -// }, -// ); - -// type ProjectsWithTodosRow = typeof projectsWithTodosQuery.Row; - -// const HomeTab: FC = () => { -// const [projectsWithTodos, projects] = useQueries([ -// projectsWithTodosQuery, -// /** -// * Load projects separately for better cache efficiency. Projects change -// * less frequently than todos, preventing unnecessary re-renders. Multiple -// * queries are fine in local-first - no network overhead. -// */ -// projectsQuery, -// ]); - -// const handleAddProjectClick = useAddProject(); - -// if (projectsWithTodos.length === 0) { -// return ( -//
-//
-// -//
-//

-// No projects yet -//

-//

-// Create your first project to get started -//

-//
-// ); -// } - -// return ( -//
-//
-// {projectsWithTodos.map((project) => ( -// -// ))} -//
-//
-// ); -// }; - -// const HomeTabProject: FC<{ -// project: ProjectsWithTodosRow; -// todos: ProjectsWithTodosRow["todos"]; -// projects: ReadonlyArray; -// }> = ({ project, todos, projects }) => { -// const { insert } = useEvolu(); -// const [newTodoTitle, setNewTodoTitle] = useState(""); - -// const addTodo = () => { -// const result = insert( -// "todo", -// { -// title: newTodoTitle.trim(), -// projectId: project.id, -// }, -// { -// onComplete: () => { -// setNewTodoTitle(""); -// }, -// }, -// ); - -// if (!result.ok) { -// alert(formatTypeError(result.error)); -// } -// }; - -// const handleKeyPress = (e: KeyboardEvent) => { -// if (e.key === "Enter") { -// addTodo(); -// } -// }; - -// return ( -//
-//
-//

-// -// {project.name} -//

-//
- -// {todos.length > 0 && ( -//
    -// {todos.map((todo) => ( -// -// ))} -//
-// )} - -//
-// { -// setNewTodoTitle(e.target.value); -// }} -// data-1p-ignore // ignore this input from 1password, ugly hack but works -// onKeyDown={handleKeyPress} -// placeholder="Add a new todo..." -// className="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6" -// /> -//
-//
-// ); -// }; - -// const HomeTabProjectSectionTodoItem: FC<{ -// // [number] extracts the element type from the todos array -// row: ProjectsWithTodosRow["todos"][number]; -// projects: ReadonlyArray; -// }> = ({ row: { id, title, isCompleted, projectId }, projects }) => { -// const { update } = useEvolu(); - -// const handleToggleCompletedClick = () => { -// // No need to check result if a mutation can't fail. -// update("todo", { -// id, -// isCompleted: booleanToSqliteBoolean(!isCompleted), -// }); -// }; - -// const handleProjectChange = (newProjectId: ProjectId) => { -// update("todo", { id, projectId: newProjectId }); -// }; - -// const handleRenameClick = () => { -// const newTitle = window.prompt("Edit todo", title); -// if (newTitle == null) return; - -// const result = update("todo", { id, title: newTitle.trim() }); -// if (!result.ok) { -// alert(formatTypeError(result.error)); -// } -// }; - -// const handleDeleteClick = () => { -// update("todo", { id, isDeleted: sqliteTrue }); -// }; - -// // Demonstrate history tracking. Evolu automatically tracks all changes -// // in the evolu_history table, making it easy to build audit logs or undo features. -// const titleHistoryQuery = createQuery((db) => -// db -// .selectFrom("evolu_history") -// .select(["value", "timestamp"]) -// .where("table", "==", "todo") -// .where("id", "==", idToIdBytes(id)) -// .where("column", "==", "title") -// // The value isn't typed; this is how we can cast it. -// .$narrowType<{ value: (typeof Schema)["todo"]["title"]["Type"] }>() -// .orderBy("timestamp", "desc"), -// ); - -// const handleHistoryClick = () => { -// void evolu.loadQuery(titleHistoryQuery).then((rows) => { -// const rowsWithTimestamp = rows.map((row) => ({ -// value: row.value, -// timestamp: timestampToDateIso(timestampBytesToTimestamp(row.timestamp)), -// })); -// alert(JSON.stringify(rowsWithTimestamp, null, 2)); -// }); -// }; - -// return ( -//
  • -// -//
    -//
    -// -// -// -// -// -//
    -// {projects.map((project) => ( -// -// -// -// ))} -//
    -//
    -//
    -// -// -// -//
    -//
    -//
  • -// ); -// }; - -// const projectsQuery = createQuery((db) => -// db -// .selectFrom("project") -// .select(["id", "name", "fooJson"]) -// .where("isDeleted", "is not", sqliteTrue) -// .where("name", "is not", null) -// .$narrowType<{ name: kysely.NotNull }>() -// .where("fooJson", "is not", null) -// .$narrowType<{ fooJson: kysely.NotNull }>() -// .orderBy("createdAt"), -// ); - -// type ProjectsRow = typeof projectsQuery.Row; - -// const useAddProject = () => { -// const { insert } = useEvolu(); - -// return () => { -// const name = window.prompt("What's the project name?"); -// if (name == null) return; - -// // Demonstrate JSON usage. -// const foo = Foo.from({ foo: "baz", bar: 42 }); -// if (!foo.ok) return; - -// const result = insert("project", { -// name: name.trim(), -// fooJson: fooToFooJson(foo.value), -// }); -// if (!result.ok) { -// alert(formatTypeError(result.error)); -// } -// }; -// }; - -// const ProjectsTab: FC = () => { -// const projects = useQuery(projectsQuery); -// const handleAddProjectClick = useAddProject(); - -// return ( -//
    -//
    -// {projects.map((project) => ( -// -// ))} -//
    -//
    -//
    -//
    -// ); -// }; - -// const ProjectsTabProjectItem: FC<{ -// project: ProjectsRow; -// }> = ({ project }) => { -// const { update } = useEvolu(); - -// const handleRenameClick = () => { -// const newName = window.prompt("Edit project name", project.name); -// if (newName == null) return; - -// const result = update("project", { id: project.id, name: newName.trim() }); -// if (!result.ok) { -// alert(formatTypeError(result.error)); -// } -// }; - -// const handleDeleteClick = () => { -// if (confirm(`Are you sure you want to delete project "${project.name}"?`)) { -// /** -// * In a classic centralized client-server app, we would fetch all todos -// * for this project and delete them too. But that approach is wrong for -// * distributed eventually consistent systems for two reasons: -// * -// * 1. Sync overhead scales with todo count (a project with 10k todos would -// * generate 10k sync messages instead of just 1 for the project) -// * 2. It wouldn't delete todos from other devices before they sync -// * -// * The correct approach for local-first systems: handle cascading logic in -// * the UI layer. Queries filter out deleted projects, so their todos -// * naturally become hidden. If a todo detail view is needed, it should -// * check whether its parent project was deleted. -// */ -// update("project", { -// id: project.id, -// isDeleted: sqliteTrue, -// }); -// } -// }; - -// // Demonstrate JSON deserialization. Because FooJson is a branded type, -// // we can safely deserialize without validation - TypeScript guarantees -// // only validated JSON strings can have the FooJson brand. -// const _foo = fooJsonToFoo(project.fooJson); - -// return ( -//
    -//
    -// -//
    -//

    {project.name}

    -//
    -//
    -//
    -// -// -//
    -//
    -// ); -// }; - -// const AccountTab: FC = () => { -// const evolu = useEvolu(); -// const appOwner = use(evolu.appOwner); - -// const [showMnemonic, setShowMnemonic] = useState(false); - -// const handleRestoreAppOwnerClick = () => { -// const mnemonic = window.prompt("Enter your mnemonic to restore your data:"); -// if (mnemonic == null) return; - -// const result = Mnemonic.from(mnemonic.trim()); -// if (!result.ok) { -// alert(formatTypeError(result.error)); -// return; -// } - -// // void evolu.restoreAppOwner(result.value); -// }; - -// const handleResetAppOwnerClick = () => { -// if (confirm("Are you sure? This will delete all your local data.")) { -// // void evolu.resetAppOwner(); -// } -// }; - -// const handleDownloadDatabaseClick = () => { -// void evolu.exportDatabase().then((data) => { -// using objectUrl = createObjectURL( -// new Blob([data], { type: "application/x-sqlite3" }), -// ); - -// const link = document.createElement("a"); -// link.href = objectUrl.url; -// link.download = `${evolu.name}.sqlite3`; -// link.click(); -// }); -// }; - -// return ( -//
    -//

    -// Todos are stored in local SQLite. When you sync across devices, your -// data is end-to-end encrypted using your mnemonic. -//

    - -//
    -//