├── .nvmrc ├── .node-version ├── packages ├── deep-freeze │ ├── .gitignore │ ├── src │ │ ├── index.ts │ │ ├── config.ts │ │ ├── hasOwn.ts │ │ ├── asserts.ts │ │ ├── frozen.ts │ │ ├── cookies.ts │ │ └── deep-freeze.ts │ ├── types │ │ └── globals.d.ts │ ├── tsconfig.json │ ├── README.md │ └── package.json ├── example │ ├── server │ │ ├── .gitignore │ │ ├── types │ │ │ └── replicache.d.ts │ │ ├── nodemon.json │ │ ├── tsconfig.json │ │ ├── src │ │ │ └── server.ts │ │ └── package.json │ ├── client-shared │ │ ├── src │ │ │ ├── index.ts │ │ │ └── space.ts │ │ ├── types │ │ │ ├── replicache.d.ts │ │ │ └── globals.d.ts │ │ ├── tsconfig.json │ │ └── package.json │ ├── web-react │ │ ├── .env.example │ │ ├── .prettierignore │ │ ├── src │ │ │ ├── index.css │ │ │ ├── vite-env.d.ts │ │ │ ├── components │ │ │ │ ├── README.md │ │ │ │ ├── header.tsx │ │ │ │ ├── link.tsx │ │ │ │ ├── todo-list.tsx │ │ │ │ ├── footer.tsx │ │ │ │ ├── todo-text-input.tsx │ │ │ │ ├── todo-item.tsx │ │ │ │ └── main-section.tsx │ │ │ ├── app.tsx │ │ │ ├── index.tsx │ │ │ └── assets │ │ │ │ └── react.svg │ │ ├── .eslintignore │ │ ├── public │ │ │ └── replicache-logo-96.png │ │ ├── tsconfig.json │ │ ├── tsconfig.node.json │ │ ├── env.d.ts │ │ ├── .gitignore │ │ ├── vite.config.ts │ │ ├── index.html │ │ ├── package.json │ │ └── vite.config.ts.timestamp-1676753335795.mjs │ ├── mobile-react-native │ │ ├── .gitignore │ │ ├── index.js │ │ ├── types │ │ │ ├── globals.d.ts │ │ │ └── react-native__assets-registry__registry.d.ts │ │ ├── react-native.config.js │ │ ├── assets │ │ │ ├── icon.png │ │ │ ├── splash.png │ │ │ ├── favicon.png │ │ │ └── adaptive-icon.png │ │ ├── babel.config.js │ │ ├── eas.json │ │ ├── tsconfig.json │ │ ├── src │ │ │ ├── index.ts │ │ │ ├── crypto-polyfill.ts │ │ │ ├── components │ │ │ │ ├── todo-input.tsx │ │ │ │ ├── todo-item.tsx │ │ │ │ └── todo-list.tsx │ │ │ ├── app.tsx │ │ │ └── use-replicache.ts │ │ ├── app.json │ │ ├── metro.config.js │ │ └── package.json │ └── shared │ │ ├── src │ │ ├── index.ts │ │ ├── todo.ts │ │ └── mutators.ts │ │ ├── types │ │ └── replicache.d.ts │ │ ├── tsconfig.json │ │ └── package.json ├── react-native-op-sqlite │ ├── .gitignore │ ├── types │ │ └── replicache.d.ts │ ├── tsconfig.json │ ├── src │ │ ├── index.ts │ │ └── replicache-op-sqlite-transaction.ts │ ├── package.json │ └── README.md ├── react-native-expo-sqlite │ ├── .gitignore │ ├── types │ │ └── replicache.d.ts │ ├── tsconfig.json │ ├── src │ │ ├── index.ts │ │ └── replicache-expo-sqlite-transaction.ts │ ├── package.json │ └── README.md └── replicache-generic-sqlite │ ├── .gitignore │ ├── types │ └── replicache.d.ts │ ├── tsconfig.json │ ├── src │ ├── index.ts │ ├── generic-sqlite-adapter.ts │ ├── replicache-generic-sqlite-write-impl.ts │ ├── replicache-generic-sqlite-read-impl.ts │ ├── replicache-generic-sqlite-database-manager.ts │ └── replicache-generic-sqlite-store.ts │ ├── README.md │ └── package.json ├── .tool-versions ├── .eslintrc ├── syncpack-snapshot.txt ├── .yarnrc.yml ├── .gitignore ├── .vscode └── settings.json ├── turbo.json ├── .github └── workflows │ └── ci.yml ├── package.json ├── README.md └── .yarn └── plugins └── @yarnpkg └── plugin-workspace-tools.cjs /.nvmrc: -------------------------------------------------------------------------------- 1 | v16.19 2 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | v16.19 2 | -------------------------------------------------------------------------------- /packages/deep-freeze/.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /packages/example/server/.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /packages/react-native-op-sqlite/.gitignore: -------------------------------------------------------------------------------- 1 | dist -------------------------------------------------------------------------------- /packages/react-native-expo-sqlite/.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /packages/replicache-generic-sqlite/.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | nodejs 16.19.0 2 | yarn 1.22.10 3 | ruby 2.7.6 -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": "universe" 4 | } 5 | -------------------------------------------------------------------------------- /packages/example/client-shared/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./space"; 2 | -------------------------------------------------------------------------------- /packages/example/web-react/.env.example: -------------------------------------------------------------------------------- 1 | VITE_REPLICACHE_LICENSE_KEY= 2 | -------------------------------------------------------------------------------- /packages/example/mobile-react-native/.gitignore: -------------------------------------------------------------------------------- 1 | .expo 2 | ios 3 | android 4 | -------------------------------------------------------------------------------- /packages/deep-freeze/src/index.ts: -------------------------------------------------------------------------------- 1 | export { deepFreeze } from "./deep-freeze"; 2 | -------------------------------------------------------------------------------- /packages/example/web-react/.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | lib 4 | *.log -------------------------------------------------------------------------------- /packages/example/web-react/src/index.css: -------------------------------------------------------------------------------- 1 | @import 'todomvc-app-css/index.css'; 2 | -------------------------------------------------------------------------------- /packages/example/web-react/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/example/shared/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./mutators"; 2 | export * from "./todo"; 3 | -------------------------------------------------------------------------------- /packages/deep-freeze/types/globals.d.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | type Request = any; 3 | } 4 | export {}; 5 | -------------------------------------------------------------------------------- /packages/example/mobile-react-native/index.js: -------------------------------------------------------------------------------- 1 | import "expo-dev-client"; 2 | 3 | import "./src/index.ts"; 4 | -------------------------------------------------------------------------------- /packages/example/server/types/replicache.d.ts: -------------------------------------------------------------------------------- 1 | import "replicache"; 2 | 3 | declare module "replicache" { 4 | export type Request = any; 5 | } 6 | -------------------------------------------------------------------------------- /packages/example/shared/types/replicache.d.ts: -------------------------------------------------------------------------------- 1 | import "replicache"; 2 | 3 | declare module "replicache" { 4 | export type Request = any; 5 | } 6 | -------------------------------------------------------------------------------- /packages/example/web-react/.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | out 3 | tool 4 | bin 5 | .eslintrc.cjs 6 | dist 7 | lib 8 | env.d.ts 9 | vite.config.ts -------------------------------------------------------------------------------- /packages/example/client-shared/types/replicache.d.ts: -------------------------------------------------------------------------------- 1 | import "replicache"; 2 | 3 | declare module "replicache" { 4 | export type Request = any; 5 | } 6 | -------------------------------------------------------------------------------- /packages/react-native-expo-sqlite/types/replicache.d.ts: -------------------------------------------------------------------------------- 1 | import "replicache"; 2 | 3 | declare module "replicache" { 4 | export type Request = any; 5 | } 6 | -------------------------------------------------------------------------------- /packages/react-native-op-sqlite/types/replicache.d.ts: -------------------------------------------------------------------------------- 1 | import "replicache"; 2 | 3 | declare module "replicache" { 4 | export type Request = any; 5 | } 6 | -------------------------------------------------------------------------------- /syncpack-snapshot.txt: -------------------------------------------------------------------------------- 1 | ✘ expo-sqlite >=14.0.0, ~14.0.3 2 | ✘ react 18.2.0, >=18.2.0 3 | ✘ react-native 0.74.1, >=0.74.0 4 | ✘ replicache 15.0.0, >=15.0.0 5 | -------------------------------------------------------------------------------- /packages/replicache-generic-sqlite/types/replicache.d.ts: -------------------------------------------------------------------------------- 1 | import "replicache"; 2 | 3 | declare module "replicache" { 4 | export type Request = any; 5 | } 6 | -------------------------------------------------------------------------------- /packages/example/mobile-react-native/types/globals.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | 3 | declare global { 4 | export type BufferSource = ArrayBufferView | ArrayBuffer; 5 | } 6 | -------------------------------------------------------------------------------- /packages/example/mobile-react-native/react-native.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | dependencies: { 3 | ...require("expo-dev-client/dependencies"), 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /packages/example/mobile-react-native/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Braden1996/react-native-replicache/HEAD/packages/example/mobile-react-native/assets/icon.png -------------------------------------------------------------------------------- /packages/example/mobile-react-native/assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Braden1996/react-native-replicache/HEAD/packages/example/mobile-react-native/assets/splash.png -------------------------------------------------------------------------------- /packages/example/mobile-react-native/assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Braden1996/react-native-replicache/HEAD/packages/example/mobile-react-native/assets/favicon.png -------------------------------------------------------------------------------- /packages/example/web-react/public/replicache-logo-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Braden1996/react-native-replicache/HEAD/packages/example/web-react/public/replicache-logo-96.png -------------------------------------------------------------------------------- /packages/example/mobile-react-native/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function (api) { 2 | api.cache(true); 3 | return { 4 | presets: ["babel-preset-expo"], 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /packages/example/mobile-react-native/assets/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Braden1996/react-native-replicache/HEAD/packages/example/mobile-react-native/assets/adaptive-icon.png -------------------------------------------------------------------------------- /packages/example/mobile-react-native/types/react-native__assets-registry__registry.d.ts: -------------------------------------------------------------------------------- 1 | declare module "@react-native/assets-registry/registry" { 2 | export type PackagerAsset = any; 3 | } 4 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | 3 | plugins: 4 | - path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs 5 | spec: "@yarnpkg/plugin-workspace-tools" 6 | 7 | yarnPath: .yarn/releases/yarn-3.4.1.cjs 8 | -------------------------------------------------------------------------------- /packages/deep-freeze/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@braden1996/tsconfig/base.json", 3 | "compilerOptions": { 4 | "target": "ES2017", 5 | "types": ["./types/globals"] 6 | }, 7 | "include": ["src"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/deep-freeze/src/config.ts: -------------------------------------------------------------------------------- 1 | declare const __DEV__: boolean; 2 | const isProd = !__DEV__; 3 | 4 | export const skipFreeze = isProd; 5 | export const skipFrozenAsserts = isProd; 6 | export const skipAssertJSONValue = isProd; 7 | -------------------------------------------------------------------------------- /packages/example/web-react/src/components/README.md: -------------------------------------------------------------------------------- 1 | This directory contains very standard React UI components that accept plain js 2 | values as props and callbacks to invoke on events. There is nothing Replicache 3 | specific in here. 4 | -------------------------------------------------------------------------------- /packages/example/web-react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@braden1996/tsconfig/react-dom.json", 3 | "exclude": ["./dist"], 4 | "compilerOptions": { 5 | "target": "ES2017", 6 | "types": ["react", "vite/client"] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/replicache-generic-sqlite/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@braden1996/tsconfig/base.json", 3 | "compilerOptions": { 4 | "target": "ES2015", 5 | "types": ["./types/replicache"] 6 | }, 7 | "include": ["src"] 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | # Yarn 4 | .yarn/* 5 | !.yarn/patches 6 | !.yarn/plugins 7 | !.yarn/releases 8 | !.yarn/sdks 9 | !.yarn/versions 10 | 11 | # Turborepo 12 | .turbo 13 | 14 | # TypeScript 15 | tsconfig.tsbuildinfo 16 | -------------------------------------------------------------------------------- /packages/example/server/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["src"], 3 | "ext": "ts,json", 4 | "ignore": ["src/**/*.spec.ts"], 5 | "exec": "node -r dotenv/config --loader ts-node/esm --inspect --experimental-specifier-resolution=node ./src/server.ts" 6 | } 7 | -------------------------------------------------------------------------------- /packages/example/web-react/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/example/shared/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@braden1996/tsconfig/base.json", 3 | "compilerOptions": { 4 | "target": "ES2017", 5 | "rootDir": "./src", 6 | "types": ["./types/replicache"] 7 | }, 8 | "include": [ 9 | "src" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /packages/example/web-react/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | interface ImportMetaEnv { 4 | readonly VITE_REPLICACHE_LICENSE_KEY: string; 5 | // more env variables... 6 | } 7 | 8 | interface ImportMeta { 9 | readonly env: ImportMetaEnv; 10 | } 11 | -------------------------------------------------------------------------------- /packages/react-native-op-sqlite/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@braden1996/tsconfig/react-native-library.json", 3 | "compilerOptions": { 4 | "rootDir": "./src", 5 | "baseUrl": ".", 6 | "types": ["./types/replicache"] 7 | }, 8 | "include": ["src"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/react-native-expo-sqlite/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@braden1996/tsconfig/react-native-library.json", 3 | "compilerOptions": { 4 | "rootDir": "./src", 5 | "baseUrl": ".", 6 | "types": ["./types/replicache"] 7 | }, 8 | "include": ["src"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/replicache-generic-sqlite/src/index.ts: -------------------------------------------------------------------------------- 1 | export { ReplicacheGenericSQLiteDatabaseManager } from "./replicache-generic-sqlite-database-manager"; 2 | export { getCreateReplicacheSQLiteKVStore } from "./replicache-generic-sqlite-store"; 3 | export * from "./generic-sqlite-adapter"; 4 | -------------------------------------------------------------------------------- /packages/example/client-shared/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@braden1996/tsconfig/react-library.json", 3 | "compilerOptions": { 4 | "rootDir": "./src", 5 | "baseUrl": ".", 6 | "types": ["./types/replicache", "./types/globals"] 7 | }, 8 | "include": ["src"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/example/mobile-react-native/eas.json: -------------------------------------------------------------------------------- 1 | { 2 | "build": { 3 | "development": { 4 | "developmentClient": true, 5 | "distribution": "internal" 6 | }, 7 | "preview": { 8 | "distribution": "internal" 9 | }, 10 | "production": {} 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/example/mobile-react-native/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@braden1996/tsconfig/react-native.json", 3 | "compilerOptions": { 4 | "types": ["./types/globals", "./types/react-native__assets-registry__registry"] 5 | }, 6 | "include": ["**/*.ts", "**/*.tsx"], 7 | "exclude": ["node_modules"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/example/mobile-react-native/src/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/order,import/first */ 2 | import { registerRootComponent } from "expo"; 3 | 4 | import { bootCryptoPolyfill } from "./crypto-polyfill"; 5 | 6 | bootCryptoPolyfill(); 7 | 8 | import { App } from "./app"; 9 | 10 | registerRootComponent(App); 11 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "editor.tabSize": 2, 4 | "[json]": { 5 | "editor.defaultFormatter": "vscode.json-language-features" 6 | }, 7 | "eslint.packageManager": "yarn", 8 | "[javascript]": { 9 | "editor.defaultFormatter": "dbaeumer.vscode-eslint" 10 | }, 11 | } 12 | -------------------------------------------------------------------------------- /packages/example/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../node_modules/@braden1996/tsconfig/node.json", 3 | "compilerOptions": { 4 | "moduleResolution": "node", 5 | "rootDir": "./src", 6 | "outDir": "./dist", 7 | "types": ["@types/node", "./types/replicache"] 8 | }, 9 | "include": ["src"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/deep-freeze/README.md: -------------------------------------------------------------------------------- 1 | # Deep Freeze 2 | 3 | ## Why is this needed? 4 | 5 | This package includes some snippets taken from Replicache's internals. It provides a mechanism for us to enforce immutability on the data that comes out of our Replicache store, by giving us stricter types along with some checks when in development mode. In production, it does nothing meaningful. 6 | -------------------------------------------------------------------------------- /packages/deep-freeze/src/hasOwn.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | const objectPrototypeHasOwnProperty = Object.prototype.hasOwnProperty; 3 | 4 | /** 5 | * Object.hasOwn polyfill 6 | */ 7 | export const hasOwn: (object: any, key: PropertyKey) => boolean = 8 | (Object as any).hasOwn || 9 | ((object, key) => objectPrototypeHasOwnProperty.call(object, key)); 10 | -------------------------------------------------------------------------------- /packages/deep-freeze/src/asserts.ts: -------------------------------------------------------------------------------- 1 | function invalidType(v: unknown, t: string): string { 2 | let s = "Invalid type: "; 3 | if (v === null || v === undefined) { 4 | s += v; 5 | } else { 6 | s += `${typeof v} \`${v}\``; 7 | } 8 | return s + `, expected ${t}`; 9 | } 10 | 11 | export function throwInvalidType(v: unknown, t: string): never { 12 | throw new Error(invalidType(v, t)); 13 | } 14 | -------------------------------------------------------------------------------- /packages/example/web-react/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | .env 26 | -------------------------------------------------------------------------------- /packages/example/web-react/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 | plugins: [react()], 7 | build: { 8 | target: 'esnext', 9 | }, 10 | server: { 11 | proxy: { 12 | '/api': { 13 | target: 'http://127.0.0.1:8080', 14 | }, 15 | }, 16 | }, 17 | }); 18 | -------------------------------------------------------------------------------- /packages/example/web-react/src/components/header.tsx: -------------------------------------------------------------------------------- 1 | import TodoTextInput from "./todo-text-input"; 2 | 3 | const Header = ({ onNewItem }: { onNewItem: (text: string) => void }) => ( 4 |
5 |

todos

6 | 11 |
12 | ); 13 | 14 | export default Header; 15 | -------------------------------------------------------------------------------- /packages/example/web-react/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Replicache Todo 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /packages/example/web-react/src/components/link.tsx: -------------------------------------------------------------------------------- 1 | import classnames from "classnames"; 2 | 3 | const Link = ({ 4 | children, 5 | selected, 6 | onClick, 7 | }: { 8 | children: any; 9 | selected: boolean; 10 | onClick: () => void; 11 | }) => { 12 | return ( 13 | onClick()} 17 | > 18 | {children} 19 | 20 | ); 21 | }; 22 | 23 | export default Link; 24 | -------------------------------------------------------------------------------- /packages/example/mobile-react-native/src/crypto-polyfill.ts: -------------------------------------------------------------------------------- 1 | import * as Crypto from "expo-crypto"; 2 | 3 | declare const global: { 4 | crypto: { 5 | getRandomValues(array: Uint8Array): Uint8Array; 6 | randomUUID(): string; 7 | }; 8 | }; 9 | 10 | export function bootCryptoPolyfill() { 11 | global.crypto = { 12 | getRandomValues(array: Uint8Array) { 13 | return Crypto.getRandomValues(array); 14 | }, 15 | randomUUID() { 16 | return Crypto.randomUUID(); 17 | }, 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /packages/deep-freeze/src/frozen.ts: -------------------------------------------------------------------------------- 1 | declare const frozenJSONTag: unique symbol; 2 | 3 | /** 4 | * Used to mark a type as having been frozen. 5 | */ 6 | export type FrozenTag = T & { readonly [frozenJSONTag]: true }; 7 | 8 | export type FrozenJSONValue = 9 | | null 10 | | string 11 | | boolean 12 | | number 13 | | FrozenJSONArray 14 | | FrozenJSONObject; 15 | 16 | type FrozenJSONArray = FrozenTag; 17 | 18 | export type FrozenJSONObject = FrozenTag<{ 19 | readonly [key: string]: FrozenJSONValue; 20 | }>; 21 | -------------------------------------------------------------------------------- /packages/example/shared/src/todo.ts: -------------------------------------------------------------------------------- 1 | // This file defines our Todo domain type in TypeScript, and a related helper 2 | // function to get all Todos. You'd typically have one of these files for each 3 | // domain object in your application. 4 | 5 | import type { ReadTransaction } from "replicache"; 6 | 7 | export type Todo = { 8 | id: string; 9 | text: string; 10 | completed: boolean; 11 | sort: number; 12 | }; 13 | 14 | export type TodoUpdate = Partial & Pick; 15 | 16 | export async function listTodos(tx: ReadTransaction) { 17 | return (await tx.scan().values().toArray()) as Todo[]; 18 | } 19 | -------------------------------------------------------------------------------- /packages/example/web-react/src/components/todo-list.tsx: -------------------------------------------------------------------------------- 1 | import { Todo, TodoUpdate } from "@react-native-replicache/example-shared"; 2 | 3 | import { TodoItem } from "./todo-item"; 4 | 5 | const TodoList = ({ 6 | todos, 7 | onUpdateTodo, 8 | onDeleteTodo, 9 | }: { 10 | todos: Todo[]; 11 | onUpdateTodo: (update: TodoUpdate) => void; 12 | onDeleteTodo: (id: string) => void; 13 | }) => { 14 | return ( 15 |
    16 | {todos.map((todo) => ( 17 | onUpdateTodo(update)} 21 | onDelete={() => onDeleteTodo(todo.id)} 22 | /> 23 | ))} 24 |
25 | ); 26 | }; 27 | 28 | export default TodoList; 29 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "pipeline": { 3 | "build": { 4 | "dependsOn": [ 5 | "^build" 6 | ], 7 | "outputs": [ 8 | "dist/**" 9 | ], 10 | "cache": false 11 | }, 12 | "test": { 13 | "dependsOn": [ 14 | "^test-typescript", 15 | "^lint", 16 | "^build" 17 | ], 18 | "cache": false 19 | }, 20 | "clean": { 21 | "outputs": [ 22 | "dist/**" 23 | ], 24 | "cache": false 25 | }, 26 | "test-typescript": { 27 | "outputs": [], 28 | "cache": false 29 | }, 30 | "lint": { 31 | "outputs": [], 32 | "cache": false 33 | }, 34 | "lint-fix": { 35 | "cache": false 36 | }, 37 | "dev": { 38 | "cache": false 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/example/client-shared/types/globals.d.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | type RequestInput = string; 3 | type RequestInit = Record; 4 | 5 | interface Body { 6 | readonly bodyUsed: boolean; 7 | json(): Promise; 8 | } 9 | 10 | interface Response extends Object, Body { 11 | readonly headers: Record; 12 | readonly ok: boolean; 13 | readonly status: number; 14 | readonly statusText: string; 15 | readonly url: string; 16 | readonly redirected: boolean; 17 | clone(): Response; 18 | } 19 | 20 | // The implementation of fetch is determined by the consuming client, e.g. DOM browser or React Native. 21 | export function fetch( 22 | input: RequestInput, 23 | init?: RequestInit 24 | ): Promise; 25 | } 26 | export {}; 27 | -------------------------------------------------------------------------------- /packages/replicache-generic-sqlite/README.md: -------------------------------------------------------------------------------- 1 | # React Native Replicache - Generic SQLite 2 | 3 | > Plug-in React Native compatibility bindings for [Replicache](https://replicache.dev/). 4 | 5 | 6 | 7 | ## Replicache version compatibility 8 | 9 | - 1.0.0 : replicache <= 14.2.2 10 | - 1.3.0 : replicache >= 15 11 | 12 | ## Why is this needed? 13 | 14 | This package provides a generic SQLite implementation of [`kvStore`](https://doc.replicache.dev/api/interfaces/ReplicacheOptions#kvstoree) that is agnostic of the underlying SQLite binding. This abstraction enables us to easily support multiple SQLite bindings. It isn't coupled to React Native either, so could work on other platforms - but that remains to be explored. 15 | -------------------------------------------------------------------------------- /packages/replicache-generic-sqlite/src/generic-sqlite-adapter.ts: -------------------------------------------------------------------------------- 1 | export interface GenericSQLResultSetRowList { 2 | length: number; 3 | item(index: number): any; 4 | } 5 | 6 | export abstract class ReplicacheGenericSQLiteTransaction { 7 | public abstract start(readonly?: boolean): Promise; 8 | 9 | public abstract execute( 10 | sqlStatement: string, 11 | args?: (string | number | null)[] | undefined, 12 | ): Promise; 13 | 14 | public abstract commit(): Promise; 15 | } 16 | 17 | export interface GenericSQLDatabase { 18 | transaction: () => ReplicacheGenericSQLiteTransaction; 19 | destroy: () => Promise; 20 | close: () => Promise; 21 | } 22 | 23 | export interface GenericDatabaseManager { 24 | open: (name: string) => Promise; 25 | } 26 | -------------------------------------------------------------------------------- /packages/example/client-shared/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@react-native-replicache/example-client-shared", 3 | "private": true, 4 | "version": "0.0.0", 5 | "main": "./src/index.ts", 6 | "module": "./src/index.ts", 7 | "types": "./src/index.ts", 8 | "type": "module", 9 | "scripts": { 10 | "test": "yarn run test-typescript", 11 | "test-typescript": "yarn run root:tsc --noEmit", 12 | "lint": "yarn run root:eslint ./src --ext .js,.ts,.tsx", 13 | "lint-fix": "yarn run lint --fix", 14 | "clean": "yarn run root:rimraf .turbo" 15 | }, 16 | "devDependencies": { 17 | "@braden1996/tsconfig": "^0.0.1" 18 | }, 19 | "peerDependencies": { 20 | "react": ">=18.2.0", 21 | "replicache": "15.0.0", 22 | "replicache-react": "^2.10.0" 23 | }, 24 | "eslintConfig": { 25 | "extends": "universe" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/replicache-generic-sqlite/src/replicache-generic-sqlite-write-impl.ts: -------------------------------------------------------------------------------- 1 | import type { KVStore, ReadonlyJSONValue } from "replicache"; 2 | 3 | import { ReplicacheGenericSQLiteReadImpl } from "./replicache-generic-sqlite-read-impl"; 4 | 5 | export class ReplicacheGenericSQLiteWriteImpl 6 | extends ReplicacheGenericSQLiteReadImpl 7 | implements Awaited> 8 | { 9 | async put(key: string, value: ReadonlyJSONValue) { 10 | const jsonValueString = JSON.stringify(value); 11 | await this._assertTx().execute( 12 | "INSERT OR REPLACE INTO entry (key, value) VALUES (?, ?)", 13 | [key, jsonValueString], 14 | ); 15 | } 16 | 17 | async del(key: string) { 18 | await this._assertTx().execute("DELETE FROM entry WHERE key = ?", [key]); 19 | } 20 | 21 | async commit() { 22 | // Do nothing and wait for release. 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/example/shared/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@react-native-replicache/example-shared", 3 | "private": true, 4 | "version": "0.0.0", 5 | "main": "./src/index.ts", 6 | "module": "./src/index.ts", 7 | "types": "./src/index.ts", 8 | "type": "module", 9 | "scripts": { 10 | "test": "yarn run test-typescript", 11 | "test-typescript": "yarn run root:tsc --noEmit", 12 | "lint": "yarn run root:eslint ./src --ext .js,.ts,.tsx", 13 | "lint-fix": "yarn run lint --fix", 14 | "clean": "yarn run root:rimraf dist .turbo" 15 | }, 16 | "devDependencies": { 17 | "@braden1996/tsconfig": "^0.0.1", 18 | "@types/express": "^4.17.13", 19 | "@types/node": "^16.11.50", 20 | "nodemon": "^2.0.19", 21 | "ts-node": "^10.9.1" 22 | }, 23 | "peerDependencies": { 24 | "replicache": ">=15.0.0" 25 | }, 26 | "eslintConfig": { 27 | "extends": "universe" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/example/mobile-react-native/src/components/todo-input.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { StyleSheet, TextInput } from "react-native"; 3 | 4 | interface TodoInputProps { 5 | handleSubmit: (text: string) => void; 6 | } 7 | 8 | export function TodoInput({ handleSubmit }: TodoInputProps) { 9 | const [draftText, setDraftText] = React.useState(""); 10 | 11 | return ( 12 | handleSubmit(draftText)} 17 | style={styles.textInput} 18 | placeholder="What needs to be done?" 19 | /> 20 | ); 21 | } 22 | 23 | const styles = StyleSheet.create({ 24 | textInput: { 25 | flex: 1, 26 | backgroundColor: "white", 27 | paddingHorizontal: 24, 28 | paddingVertical: 16, 29 | borderTopLeftRadius: 8, 30 | borderTopRightRadius: 8, 31 | borderBottomWidth: StyleSheet.hairlineWidth, 32 | borderColor: "#e6e6e6", 33 | }, 34 | }); 35 | -------------------------------------------------------------------------------- /packages/react-native-op-sqlite/src/index.ts: -------------------------------------------------------------------------------- 1 | import * as OPSQLite from "@op-engineering/op-sqlite"; 2 | import { 3 | GenericDatabaseManager, 4 | getCreateReplicacheSQLiteKVStore, 5 | ReplicacheGenericSQLiteDatabaseManager, 6 | } from "@react-native-replicache/replicache-generic-sqlite"; 7 | 8 | import { ReplicacheOPSQLiteTransaction } from "./replicache-op-sqlite-transaction"; 9 | 10 | const genericDatabase: GenericDatabaseManager = { 11 | open: async (name: string) => { 12 | const db = OPSQLite.open({ name }); 13 | 14 | return { 15 | transaction: () => new ReplicacheOPSQLiteTransaction(db), 16 | destroy: async () => db.delete(), 17 | close: async () => db.close(), 18 | }; 19 | }, 20 | }; 21 | 22 | const opSqlManagerInstance = new ReplicacheGenericSQLiteDatabaseManager( 23 | genericDatabase, 24 | ); 25 | 26 | export const createReplicacheReactNativeOPSQLiteKVStore = { 27 | create: getCreateReplicacheSQLiteKVStore(opSqlManagerInstance), 28 | drop: (name: string) => opSqlManagerInstance.destroy(name), 29 | }; 30 | -------------------------------------------------------------------------------- /packages/deep-freeze/src/cookies.ts: -------------------------------------------------------------------------------- 1 | import type { ReadonlyJSONValue } from "replicache"; 2 | 3 | import type { FrozenJSONValue } from "./frozen"; 4 | 5 | /** 6 | * A cookie is a value that is used to determine the order of snapshots. It 7 | * needs to be comparable. This can be a `string`, `number` or if you want to 8 | * use a more complex value, you can use an object with an `order` property. The 9 | * value `null` is considered to be less than any other cookie and it is used 10 | * for the first pull when no cookie has been set. 11 | * 12 | * The order is the natural order of numbers and strings. If one of the cookies 13 | * is an object then the value of the `order` property is treated as the cookie 14 | * when doing comparison. 15 | * 16 | * If one of the cookies is a string and the other is a number, the number is 17 | * fist converted to a string (using `toString()`). 18 | */ 19 | export type Cookie = 20 | | null 21 | | string 22 | | number 23 | | (ReadonlyJSONValue & { readonly order: number | string }); 24 | 25 | export type FrozenCookie = 26 | | null 27 | | string 28 | | number 29 | | (FrozenJSONValue & { readonly order: number | string }); 30 | -------------------------------------------------------------------------------- /packages/example/mobile-react-native/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "React Native Replicache", 4 | "slug": "react-native-replicache", 5 | "version": "1.1.0", 6 | "orientation": "portrait", 7 | "icon": "./assets/icon.png", 8 | "userInterfaceStyle": "light", 9 | "splash": { 10 | "image": "./assets/splash.png", 11 | "resizeMode": "contain", 12 | "backgroundColor": "#ffffff" 13 | }, 14 | "updates": { 15 | "fallbackToCacheTimeout": 0 16 | }, 17 | "assetBundlePatterns": [ 18 | "**/*" 19 | ], 20 | "ios": { 21 | "supportsTablet": true, 22 | "bundleIdentifier": "com.braden1996.rnreplicache" 23 | }, 24 | "android": { 25 | "adaptiveIcon": { 26 | "foregroundImage": "./assets/adaptive-icon.png", 27 | "backgroundColor": "#FFFFFF" 28 | }, 29 | "package": "com.braden1996.rnreplicache" 30 | }, 31 | "plugins": [ 32 | [ 33 | "expo-build-properties", 34 | { 35 | "android": { 36 | "flipper": "0.174.0" 37 | }, 38 | "ios": { 39 | "flipper": "0.174.0" 40 | } 41 | } 42 | ] 43 | ] 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/react-native-expo-sqlite/src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GenericDatabaseManager, 3 | GenericSQLDatabase, 4 | getCreateReplicacheSQLiteKVStore, 5 | ReplicacheGenericSQLiteDatabaseManager, 6 | } from "@react-native-replicache/replicache-generic-sqlite"; 7 | import * as SQLite from "expo-sqlite"; 8 | 9 | import { ReplicacheExpoSQLiteTransaction } from "./replicache-expo-sqlite-transaction"; 10 | 11 | const genericDatabase: GenericDatabaseManager = { 12 | open: async (name: string) => { 13 | const db = await SQLite.openDatabaseAsync(name); 14 | 15 | const genericDb: GenericSQLDatabase = { 16 | transaction: () => new ReplicacheExpoSQLiteTransaction(db), 17 | destroy: async () => { 18 | await db.closeAsync(); 19 | await SQLite.deleteDatabaseAsync(name); 20 | }, 21 | close: async () => await db.closeAsync(), 22 | }; 23 | 24 | return genericDb; 25 | }, 26 | }; 27 | 28 | const expoDbManagerInstance = new ReplicacheGenericSQLiteDatabaseManager( 29 | genericDatabase, 30 | ); 31 | 32 | export const createReplicacheExpoSQLiteKVStore = { 33 | create: getCreateReplicacheSQLiteKVStore(expoDbManagerInstance), 34 | drop: (name: string) => expoDbManagerInstance.destroy(name), 35 | }; 36 | -------------------------------------------------------------------------------- /packages/example/mobile-react-native/metro.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | /* eslint-disable @typescript-eslint/no-var-requires */ 3 | const { getDefaultConfig } = require("expo/metro-config"); 4 | const getWorkspaces = require("get-yarn-workspaces"); 5 | const path = require("node:path"); 6 | 7 | /** 8 | * @param {import('expo/metro-config').MetroConfig} config 9 | * @param {string} dirname 10 | */ 11 | function mutateConfigForMonorepo(config, dirname) { 12 | const workspaces = getWorkspaces(dirname).filter( 13 | (pathItem) => !path.extname(pathItem), 14 | ); 15 | const workspaceRoot = path.resolve(dirname, ".."); 16 | 17 | config.resolver.watchFolders = [ 18 | dirname, 19 | path.resolve(dirname, "../node_modules"), 20 | ...workspaces, 21 | ]; 22 | 23 | config.resolver.nodeModulesPaths = [ 24 | path.join(dirname, "./node_modules"), 25 | path.join(workspaceRoot, "./node_modules"), 26 | ...workspaces.map((directory) => path.resolve(directory, "node_modules")), 27 | ]; 28 | } 29 | 30 | function getMetroConfig(dirname) { 31 | const config = getDefaultConfig(dirname); 32 | 33 | mutateConfigForMonorepo(config, dirname); 34 | 35 | return config; 36 | } 37 | 38 | module.exports = getMetroConfig(__dirname); 39 | -------------------------------------------------------------------------------- /packages/example/web-react/src/components/footer.tsx: -------------------------------------------------------------------------------- 1 | import FilterLink from "./link"; 2 | 3 | const FILTER_TITLES = ["All", "Active", "Completed"]; 4 | 5 | const Footer = ({ 6 | active, 7 | completed, 8 | currentFilter, 9 | onFilter, 10 | onDeleteCompleted, 11 | }: { 12 | active: number; 13 | completed: number; 14 | currentFilter: string; 15 | onFilter: (filter: string) => void; 16 | onDeleteCompleted: () => void; 17 | }) => { 18 | const itemWord = active === 1 ? "item" : "items"; 19 | return ( 20 |
21 | 22 | {active || "No"} {itemWord} left 23 | 24 |
    25 | {FILTER_TITLES.map((filter) => ( 26 |
  • 27 | onFilter(filter)} 29 | selected={filter === currentFilter} 30 | > 31 | {filter} 32 | 33 |
  • 34 | ))} 35 |
36 | {completed > 0 && ( 37 | 40 | )} 41 |
42 | ); 43 | }; 44 | 45 | export default Footer; 46 | -------------------------------------------------------------------------------- /packages/example/web-react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@react-native-replicache/example-web-react", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "start": "vite start", 9 | "build": "vite build", 10 | "test": "yarn run test-typescript", 11 | "test-typescript": "yarn run root:tsc --noEmit", 12 | "lint": "yarn run root:eslint ./src --ext .js,.ts,.tsx", 13 | "lint-fix": "yarn run lint --fix", 14 | "clean": "yarn run root:rimraf dist .turbo" 15 | }, 16 | "dependencies": { 17 | "@react-native-replicache/example-client-shared": "0.0.0", 18 | "@react-native-replicache/example-shared": "0.0.0", 19 | "classnames": "^2.3.1", 20 | "nanoid": "^3.3.7", 21 | "qs": "^6.11.0", 22 | "react": "18.2.0", 23 | "react-dom": "18.2.0", 24 | "replicache": "15.0.0", 25 | "replicache-react": "^2.10.0", 26 | "todomvc-app-css": "^2.4.2" 27 | }, 28 | "devDependencies": { 29 | "@braden1996/tsconfig": "^0.0.1", 30 | "@types/babel__core": "^7.20.0", 31 | "@types/react": "~18.2.79", 32 | "@types/react-dom": "~18.2.25", 33 | "@vitejs/plugin-react": "^2.0.1", 34 | "typescript": "~5.4.5", 35 | "vite": "^3.0.7" 36 | }, 37 | "eslintConfig": { 38 | "extends": "universe/web" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/example/client-shared/src/space.ts: -------------------------------------------------------------------------------- 1 | export class Space { 2 | constructor(private readonly fetchHost: string) {} 3 | 4 | public async exists(spaceID: string): Promise { 5 | const spaceExistRes = await this.fetchJSON("spaceExists", spaceID); 6 | if ( 7 | spaceExistRes && 8 | typeof spaceExistRes === "object" && 9 | typeof spaceExistRes.spaceExists === "boolean" 10 | ) { 11 | return spaceExistRes.spaceExists; 12 | } 13 | throw new Error("Bad response from spaceExists"); 14 | } 15 | 16 | public async create(spaceID?: string): Promise { 17 | const createSpaceRes = await this.fetchJSON("createSpace", spaceID); 18 | if ( 19 | createSpaceRes && 20 | typeof createSpaceRes === "object" && 21 | typeof createSpaceRes.spaceID === "string" 22 | ) { 23 | return createSpaceRes.spaceID; 24 | } 25 | throw new Error("Bad response from createSpace"); 26 | } 27 | 28 | private async fetchJSON(apiName: string, spaceID: string | undefined) { 29 | const res = await fetch(`${this.fetchHost}/api/replicache/${apiName}`, { 30 | method: "POST", 31 | headers: { 32 | "Content-Type": "application/json", 33 | }, 34 | body: 35 | spaceID && 36 | JSON.stringify({ 37 | spaceID, 38 | }), 39 | }); 40 | return await res.json(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/example/web-react/src/components/todo-text-input.tsx: -------------------------------------------------------------------------------- 1 | import classnames from "classnames"; 2 | import { 3 | ChangeEvent, 4 | FocusEvent, 5 | KeyboardEvent, 6 | useRef, 7 | useState, 8 | } from "react"; 9 | 10 | export default function TodoTextInput({ 11 | initial, 12 | placeholder, 13 | onBlur, 14 | onSubmit, 15 | }: { 16 | initial: string; 17 | placeholder?: string; 18 | onBlur?: (text: string) => void; 19 | onSubmit: (text: string) => void; 20 | }) { 21 | const [textInput, setTextInput] = useState(initial); 22 | const ref = useRef(null); 23 | 24 | const handleSubmit = async (e: KeyboardEvent) => { 25 | if (e.key === "Enter") { 26 | onSubmit(textInput); 27 | setTextInput(""); 28 | } 29 | }; 30 | 31 | const handleChange = (e: ChangeEvent) => { 32 | setTextInput(e.target.value); 33 | }; 34 | 35 | const handleBlur = (_e: FocusEvent) => { 36 | if (onBlur) { 37 | onBlur(textInput); 38 | } 39 | }; 40 | 41 | return ( 42 | 56 | ); 57 | } 58 | -------------------------------------------------------------------------------- /packages/example/server/src/server.ts: -------------------------------------------------------------------------------- 1 | import "dotenv/config"; 2 | import { mutators } from "@react-native-replicache/example-shared"; 3 | import express from "express"; 4 | import fs from "fs"; 5 | import path from "path"; 6 | import { ReplicacheExpressServer } from "replicache-express"; 7 | import { fileURLToPath } from "url"; 8 | const __filename = fileURLToPath(import.meta.url); 9 | const __dirname = path.dirname(__filename); 10 | const portEnv = parseInt(process.env.PORT || "", 10); 11 | const port = Number.isInteger(portEnv) ? portEnv : 8080; 12 | const options = { 13 | mutators, 14 | port, 15 | host: process.env.HOST || "0.0.0.0", 16 | }; 17 | 18 | const default_dist = path.join(__dirname, "../dist/dist"); 19 | 20 | if (process.env.NODE_ENV === "production") { 21 | const r = new ReplicacheExpressServer(options); 22 | r.app.use(express.static(default_dist)); 23 | r.app.get("/health", (_req, res) => { 24 | res.send("ok"); 25 | }); 26 | r.app.use("*", (_req, res) => { 27 | const index = path.join(default_dist, "index.html"); 28 | const html = fs.readFileSync(index, "utf8"); 29 | res.status(200).set({ "Content-Type": "text/html" }).end(html); 30 | }); 31 | r.start(() => { 32 | console.log( 33 | `Replicache is listening on ${options.host}:${options.port} -- ${default_dist}`, 34 | ); 35 | }); 36 | } else { 37 | ReplicacheExpressServer.start(options, () => { 38 | console.log(`Server listening on ${options.host}:${options.port}`); 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /packages/example/mobile-react-native/src/app.tsx: -------------------------------------------------------------------------------- 1 | import { Space } from "@react-native-replicache/example-client-shared"; 2 | import { StatusBar } from "expo-status-bar"; 3 | import React from "react"; 4 | import { 5 | ActivityIndicator, 6 | SafeAreaView, 7 | StyleSheet, 8 | Text, 9 | View, 10 | } from "react-native"; 11 | 12 | import { TodoList } from "./components/todo-list"; 13 | 14 | const space = new Space("http://127.0.0.1:8080"); 15 | 16 | export function App() { 17 | const [listId, setListId] = React.useState(null); 18 | 19 | React.useEffect(() => { 20 | if (listId !== null) return; 21 | const createList = async () => { 22 | const listId = await space.create(); 23 | setListId(listId); 24 | }; 25 | createList(); 26 | }, [listId]); 27 | 28 | return ( 29 | 30 | 31 | todos 32 | 35 | {listId === null ? ( 36 | 37 | ) : ( 38 | 39 | )} 40 | 41 | 42 | ); 43 | } 44 | 45 | const styles = StyleSheet.create({ 46 | container: { 47 | flex: 1, 48 | backgroundColor: "#f5f5f5", 49 | alignItems: "stretch", 50 | }, 51 | title: { 52 | fontSize: 80, 53 | fontWeight: "200", 54 | textAlign: "center", 55 | color: "#b83f45", 56 | }, 57 | }); 58 | -------------------------------------------------------------------------------- /packages/replicache-generic-sqlite/src/replicache-generic-sqlite-read-impl.ts: -------------------------------------------------------------------------------- 1 | import { deepFreeze } from "@react-native-replicache/deep-freeze"; 2 | import type { KVStore, ReadonlyJSONValue } from "replicache"; 3 | 4 | import { ReplicacheGenericSQLiteTransaction } from "./generic-sqlite-adapter"; 5 | 6 | export class ReplicacheGenericSQLiteReadImpl 7 | implements Awaited> 8 | { 9 | protected _tx: ReplicacheGenericSQLiteTransaction | null; 10 | 11 | constructor(tx: ReplicacheGenericSQLiteTransaction) { 12 | this._tx = tx; 13 | } 14 | 15 | async has(key: string) { 16 | const unsafeValue = await this._getSql(key); 17 | return unsafeValue === undefined; 18 | } 19 | 20 | async get(key: string) { 21 | const unsafeValue = await this._getSql(key); 22 | if (unsafeValue === undefined) return; 23 | const parsedValue = JSON.parse(unsafeValue) as ReadonlyJSONValue; 24 | // @ts-ignore 25 | const frozenValue = deepFreeze(parsedValue); 26 | return frozenValue; 27 | } 28 | 29 | async release() { 30 | const tx = this._assertTx(); 31 | await tx.commit(); 32 | this._tx = null; 33 | } 34 | 35 | get closed(): boolean { 36 | return this._tx === null; 37 | } 38 | 39 | private async _getSql(key: string) { 40 | const rows = await this._assertTx().execute( 41 | "SELECT value FROM entry WHERE key = ?", 42 | [key], 43 | ); 44 | 45 | if (rows.length === 0) return undefined; 46 | 47 | return rows.item(0).value; 48 | } 49 | 50 | protected _assertTx() { 51 | if (this._tx === null) throw new Error("Transaction is closed"); 52 | return this._tx; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /packages/deep-freeze/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@react-native-replicache/deep-freeze", 3 | "version": "1.3.0", 4 | "main": "./dist/commonjs/index.js", 5 | "module": "./dist/module/index.js", 6 | "react-native": "./src/index.ts", 7 | "types": "./dist/typescript/index.d.ts", 8 | "files": [ 9 | "dist/", 10 | "src/" 11 | ], 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/braden1996/react-native-replicache.git", 15 | "directory": "packages/deep-freeze" 16 | }, 17 | "keywords": [ 18 | "deepfreeze", 19 | "braden1996" 20 | ], 21 | "author": "Braden Marshall ", 22 | "license": "MIT", 23 | "homepage": "https://github.com/braden1996/react-native-replicache", 24 | "bugs": { 25 | "url": "https://github.com/braden1996/react-native-replicache/issues" 26 | }, 27 | "scripts": { 28 | "build": "bob build", 29 | "test": "yarn run test-typescript", 30 | "test-typescript": "yarn run root:tsc --noEmit", 31 | "lint": "yarn run root:eslint ./src --ext .ts", 32 | "lint-fix": "yarn run lint --fix", 33 | "clean": "yarn run root:rimraf dist .turbo" 34 | }, 35 | "devDependencies": { 36 | "@braden1996/tsconfig": "^0.0.1", 37 | "react-native-builder-bob": "^0.20.3" 38 | }, 39 | "peerDependencies": { 40 | "replicache": ">=15.0.0" 41 | }, 42 | "react-native-builder-bob": { 43 | "source": "src", 44 | "output": "dist", 45 | "targets": [ 46 | "commonjs", 47 | "module", 48 | [ 49 | "typescript", 50 | { 51 | "tsc": "../../node_modules/.bin/tsc" 52 | } 53 | ] 54 | ] 55 | }, 56 | "eslintConfig": { 57 | "extends": "universe" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | types: [opened, synchronize] 9 | 10 | jobs: 11 | build: 12 | name: Build and Test 13 | timeout-minutes: 15 14 | runs-on: ${{ matrix.os }} 15 | strategy: 16 | matrix: 17 | os: [ubuntu-latest] 18 | 19 | steps: 20 | - name: Check out code 21 | uses: actions/checkout@v2 22 | with: 23 | fetch-depth: 2 24 | 25 | - name: Setup Node.js environment 26 | uses: actions/setup-node@v3 27 | with: 28 | node-version: 16 29 | cache: "yarn" 30 | 31 | - name: Install dependencies 32 | run: yarn install --immutable 33 | 34 | - name: Build 35 | run: yarn run build 36 | 37 | - name: Test TypeScript 38 | run: yarn run test-typescript 39 | 40 | - name: Lint 41 | run: yarn run lint 42 | 43 | check_workspace_dependency_drift: 44 | name: Check Workspace Dependency Drift 45 | runs-on: ubuntu-latest 46 | timeout-minutes: 30 47 | steps: 48 | - name: Check out code 49 | uses: actions/checkout@v2 50 | with: 51 | fetch-depth: 2 52 | 53 | - name: Setup Node.js environment 54 | uses: actions/setup-node@v3 55 | with: 56 | node-version: 16 57 | cache: "yarn" 58 | 59 | - name: Install dependencies 60 | run: yarn install --immutable 61 | 62 | - name: Generate new syncpack snapshot 63 | run: yarn run update-syncpack-snapshot 64 | shell: bash 65 | 66 | - name: Check for unexpected workspace dependency drift 67 | run: git diff --exit-code -- syncpack-snapshot.txt 68 | -------------------------------------------------------------------------------- /packages/example/web-react/src/components/todo-item.tsx: -------------------------------------------------------------------------------- 1 | import { Todo, TodoUpdate } from "@react-native-replicache/example-shared"; 2 | import classnames from "classnames"; 3 | import { useState } from "react"; 4 | 5 | import TodoTextInput from "./todo-text-input"; 6 | 7 | export function TodoItem({ 8 | todo, 9 | onUpdate, 10 | onDelete, 11 | }: { 12 | todo: Todo; 13 | onUpdate: (update: TodoUpdate) => void; 14 | onDelete: () => void; 15 | }) { 16 | const { id } = todo; 17 | const [editing, setEditing] = useState(false); 18 | 19 | const handleDoubleClick = () => { 20 | setEditing(true); 21 | }; 22 | 23 | const handleSave = (text: string) => { 24 | if (text.length === 0) { 25 | onDelete(); 26 | } else { 27 | onUpdate({ id, text }); 28 | } 29 | setEditing(false); 30 | }; 31 | 32 | const handleToggleComplete = () => 33 | onUpdate({ id, completed: !todo.completed }); 34 | 35 | let element; 36 | if (editing) { 37 | element = ( 38 | 43 | ); 44 | } else { 45 | element = ( 46 |
47 | 53 | 54 |
56 | ); 57 | } 58 | 59 | return ( 60 |
  • 66 | {element} 67 |
  • 68 | ); 69 | } 70 | -------------------------------------------------------------------------------- /packages/example/server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@react-native-replicache/example-server", 3 | "private": true, 4 | "version": "0.0.0", 5 | "main": "./src/index.ts", 6 | "type": "module", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/braden1996/react-native-replicache.git", 10 | "directory": "packages/react-native-op-sqlite" 11 | }, 12 | "engines": { 13 | "node": "16.19.x" 14 | }, 15 | "keywords": [ 16 | "react-native-replicache", 17 | "braden1996" 18 | ], 19 | "author": "Braden Marshall ", 20 | "license": "MIT", 21 | "homepage": "https://github.com/braden1996/react-native-replicache", 22 | "bugs": { 23 | "url": "https://github.com/braden1996/react-native-replicache/issues" 24 | }, 25 | "scripts": { 26 | "build": "ncc build ./src/server.ts --source-map --transpile-only", 27 | "start": "node -r dotenv/config ./dist/server.js", 28 | "dev": "NODE_ENV=development nodemon", 29 | "test": "yarn run test-typescript", 30 | "test-typescript": "yarn run root:tsc --noEmit", 31 | "lint": "yarn run root:eslint ./src --ext .js,.ts,.tsx", 32 | "lint-fix": "yarn run lint --fix", 33 | "clean": "yarn run root:rimraf dist .turbo" 34 | }, 35 | "dependencies": { 36 | "@react-native-replicache/example-shared": "0.0.0", 37 | "dotenv": "^16.0.1", 38 | "express": "^4.18.1", 39 | "pg-mem": "^2.6.4", 40 | "replicache": "15.0.0", 41 | "replicache-express": "^0.3.0-beta.2" 42 | }, 43 | "devDependencies": { 44 | "@braden1996/tsconfig": "^0.0.1", 45 | "@types/express": "^4.17.13", 46 | "@types/node": "^16.11.50", 47 | "@vercel/ncc": "^0.36.0", 48 | "nodemon": "^2.0.19", 49 | "ts-node": "^10.9.1" 50 | }, 51 | "eslintConfig": { 52 | "extends": "universe/native" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /packages/example/web-react/src/app.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | M, 3 | listTodos, 4 | TodoUpdate, 5 | } from "@react-native-replicache/example-shared"; 6 | import { nanoid } from "nanoid"; 7 | import { Replicache } from "replicache"; 8 | import { useSubscribe } from "replicache-react"; 9 | 10 | import Header from "./components/header"; 11 | import MainSection from "./components/main-section"; 12 | 13 | // This is the top-level component for our app. 14 | const App = ({ rep }: { rep: Replicache }) => { 15 | // Subscribe to all todos and sort them. 16 | const todos = useSubscribe(rep, listTodos, [], [rep]); 17 | todos.sort((a, b) => a.sort - b.sort); 18 | // Define event handlers and connect them to Replicache mutators. Each 19 | // of these mutators runs immediately (optimistically) locally, then runs 20 | // again on the server-side automatically. 21 | const handleNewItem = (text: string) => 22 | rep.mutate.createTodo({ 23 | id: nanoid(), 24 | text, 25 | completed: false, 26 | }); 27 | 28 | const handleUpdateTodo = (update: TodoUpdate) => 29 | rep.mutate.updateTodo(update); 30 | 31 | const handleDeleteTodos = async (ids: string[]) => { 32 | for (const id of ids) { 33 | await rep.mutate.deleteTodo(id); 34 | } 35 | }; 36 | 37 | const handleCompleteTodos = async (completed: boolean, ids: string[]) => { 38 | for (const id of ids) { 39 | await rep.mutate.updateTodo({ 40 | id, 41 | completed, 42 | }); 43 | } 44 | }; 45 | 46 | // Render app. 47 | 48 | return ( 49 |
    50 |
    51 | 57 |
    58 | ); 59 | }; 60 | 61 | export default App; 62 | -------------------------------------------------------------------------------- /packages/replicache-generic-sqlite/src/replicache-generic-sqlite-database-manager.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GenericDatabaseManager, 3 | GenericSQLDatabase, 4 | } from "./generic-sqlite-adapter"; 5 | 6 | export class ReplicacheGenericSQLiteDatabaseManager { 7 | private _dbInstances = new Map< 8 | string, 9 | { 10 | db: GenericSQLDatabase; 11 | state: "open" | "closed"; 12 | } 13 | >(); 14 | 15 | constructor(private readonly _dbm: GenericDatabaseManager) {} 16 | 17 | async open(name: string) { 18 | const dbInstance = this._dbInstances.get(name); 19 | if (dbInstance?.state === "open") return dbInstance.db; 20 | 21 | const newDb = await this._dbm.open(`replicache-${name}.sqlite`); 22 | if (!dbInstance) { 23 | await this._setupSchema(newDb); 24 | this._dbInstances.set(name, { state: "open", db: newDb }); 25 | } else { 26 | dbInstance.state = "open"; 27 | } 28 | 29 | return newDb; 30 | } 31 | 32 | async close(name: string) { 33 | const dbInstance = this._dbInstances.get(name); 34 | if (!dbInstance) return; 35 | 36 | await dbInstance.db.close(); 37 | dbInstance.state = "closed"; 38 | } 39 | 40 | async truncate(name: string) { 41 | const db = await this.open(name); 42 | const tx = db.transaction(); 43 | await tx.start(false); 44 | await tx.execute("DELETE FROM entry", []); 45 | await tx.commit(); 46 | } 47 | 48 | async destroy(name: string) { 49 | const dbInstances = this._dbInstances.get(name); 50 | if (!dbInstances) return; 51 | 52 | await dbInstances.db.destroy(); 53 | this._dbInstances.delete(name); 54 | } 55 | 56 | private async _setupSchema(db: GenericSQLDatabase) { 57 | const tx = db.transaction(); 58 | await tx.start(false); 59 | await tx.execute( 60 | "CREATE TABLE IF NOT EXISTS entry (key TEXT PRIMARY KEY, value TEXT)", 61 | [], 62 | ); 63 | await tx.commit(); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-replicache", 3 | "version": "0.0.0", 4 | "private": true, 5 | "engines": { 6 | "node": "16.19.x" 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/braden1996/react-native-replicache.git" 11 | }, 12 | "author": "Braden Marshall ", 13 | "license": "MIT", 14 | "homepage": "https://github.com/braden1996/react-native-replicache", 15 | "bugs": { 16 | "url": "https://github.com/braden1996/react-native-replicache/issues" 17 | }, 18 | "packageManager": "yarn@3.4.1", 19 | "scripts": { 20 | "build": "turbo run build", 21 | "dev": "turbo run dev --parallel", 22 | "test": "turbo run test", 23 | "test-typescript": "turbo run test", 24 | "lint": "turbo run lint", 25 | "lint-fix": "turbo run lint-fix", 26 | "clean": "turbo run clean", 27 | "clean-node-modules": "rm -rf **/node_modules", 28 | "graph": "turbo run build --graph=graph.pdf", 29 | "root:eslint": "cd $INIT_CWD && eslint", 30 | "root:rimraf": "cd $INIT_CWD && rimraf", 31 | "root:tsc": "cd $INIT_CWD && tsc", 32 | "update-syncpack-snapshot": "syncpack list-mismatch --filter '^(?!@react-native-replicache/)(?!react-native-replicache$).*$' | sed '/^-/d' > syncpack-snapshot.txt" 33 | }, 34 | "devDependencies": { 35 | "eslint": "8.56.0", 36 | "eslint-config-universe": "^12.1.0", 37 | "prettier": "^3.2.5", 38 | "rimraf": "^4.3.0", 39 | "syncpack": "^9.3.2", 40 | "turbo": "latest", 41 | "typescript": "~5.4.5" 42 | }, 43 | "resolutions": { 44 | "@types/react": "~18.2.79", 45 | "@types/react-dom": "~18.2.25" 46 | }, 47 | "workspaces": { 48 | "packages": [ 49 | "packages/example/*", 50 | "packages/deep-freeze", 51 | "packages/react-native-expo-sqlite", 52 | "packages/react-native-op-sqlite", 53 | "packages/replicache-generic-sqlite" 54 | ] 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /packages/replicache-generic-sqlite/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@react-native-replicache/replicache-generic-sqlite", 3 | "version": "1.3.0", 4 | "main": "./dist/commonjs/index.js", 5 | "module": "./dist/module/index.js", 6 | "react-native": "./src/index.ts", 7 | "types": "./dist/typescript/index.d.ts", 8 | "files": [ 9 | "dist/", 10 | "src/" 11 | ], 12 | "type": "module", 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/braden1996/react-native-replicache.git", 16 | "directory": "packages/replicache-generic-sqlite" 17 | }, 18 | "keywords": [ 19 | "react-native", 20 | "replicache", 21 | "sqlite", 22 | "braden1996" 23 | ], 24 | "author": "Braden Marshall ", 25 | "license": "MIT", 26 | "homepage": "https://github.com/braden1996/react-native-replicache", 27 | "bugs": { 28 | "url": "https://github.com/braden1996/react-native-replicache/issues" 29 | }, 30 | "scripts": { 31 | "build": "bob build", 32 | "test": "yarn run test-typescript", 33 | "test-typescript": "yarn run root:tsc --noEmit", 34 | "lint": "yarn run root:eslint ./src --ext .ts", 35 | "lint-fix": "yarn run lint --fix", 36 | "clean": "yarn run root:rimraf dist .turbo" 37 | }, 38 | "dependencies": { 39 | "@react-native-replicache/deep-freeze": "1.3.0" 40 | }, 41 | "devDependencies": { 42 | "@braden1996/tsconfig": "^0.0.1", 43 | "react-native-builder-bob": "^0.20.3", 44 | "replicache": "15.0.0" 45 | }, 46 | "peerDependencies": { 47 | "replicache": ">=15.0.0" 48 | }, 49 | "react-native-builder-bob": { 50 | "source": "src", 51 | "output": "dist", 52 | "targets": [ 53 | "commonjs", 54 | "module", 55 | [ 56 | "typescript", 57 | { 58 | "tsc": "../../node_modules/.bin/tsc" 59 | } 60 | ] 61 | ] 62 | }, 63 | "eslintConfig": { 64 | "extends": "universe" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /packages/example/mobile-react-native/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@react-native-replicache/example-mobile-react-native", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "expo start --dev-client", 8 | "android": "expo run:android", 9 | "ios": "expo run:ios", 10 | "prebuild": "expo prebuild", 11 | "test": "yarn run test-typescript", 12 | "test-typescript": "yarn run root:tsc --noEmit", 13 | "lint": "yarn run root:eslint ./src/**/*.{ts,tsx}", 14 | "lint-fix": "yarn run lint --fix", 15 | "clean": "yarn run root:rimraf .turbo .expo ios android", 16 | "clean-expo": "yarn run root:rimraf .expo ios android", 17 | "clean-cache": "rm -rf $TMPDIR/metro-cache ~/Library/Developer/Xcode/DerivedData/ && watchman watch-del-all", 18 | "start": "expo start --dev-client" 19 | }, 20 | "dependencies": { 21 | "@op-engineering/op-sqlite": "^6.0.1", 22 | "@react-native-replicache/example-client-shared": "0.0.0", 23 | "@react-native-replicache/example-shared": "0.0.0", 24 | "@react-native-replicache/react-native-expo-sqlite": "1.3.1", 25 | "@react-native-replicache/react-native-op-sqlite": "1.3.1", 26 | "expo": "~51.0.8", 27 | "expo-build-properties": "~0.12.1", 28 | "expo-crypto": "~13.0.2", 29 | "expo-dev-client": "~4.0.14", 30 | "expo-splash-screen": "~0.27.4", 31 | "expo-sqlite": "~14.0.3", 32 | "expo-status-bar": "~1.12.1", 33 | "nanoid": "^3.3.7", 34 | "react": "18.2.0", 35 | "react-dom": "18.2.0", 36 | "react-native": "0.74.1", 37 | "react-native-sse": "^1.1.0", 38 | "replicache": "15.0.0", 39 | "replicache-react": "^2.10.0" 40 | }, 41 | "devDependencies": { 42 | "@babel/core": "^7.24.0", 43 | "@braden1996/tsconfig": "^0.0.1", 44 | "@types/react": "~18.2.79", 45 | "@types/react-native": "~0.73.0", 46 | "get-yarn-workspaces": "^1.0.2" 47 | }, 48 | "eslintConfig": { 49 | "extends": "universe/native" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /packages/example/web-react/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { Space } from "@react-native-replicache/example-client-shared"; 2 | import { mutators } from "@react-native-replicache/example-shared"; 3 | import React from "react"; 4 | import ReactDOM from "react-dom/client"; 5 | import { Replicache, TEST_LICENSE_KEY } from "replicache"; 6 | 7 | import "./index.css"; 8 | 9 | import App from "./app"; 10 | 11 | const space = new Space(""); 12 | 13 | async function init() { 14 | const { pathname } = window.location; 15 | 16 | if (pathname === "/" || pathname === "") { 17 | window.location.href = "/list/" + (await space.create()); 18 | return; 19 | } 20 | 21 | // URL layout is "/list/" 22 | const paths = pathname.split("/"); 23 | const [, listDir, listID] = paths; 24 | if ( 25 | listDir !== "list" || 26 | listID === undefined || 27 | !(await space.exists(listID)) 28 | ) { 29 | window.location.href = "/"; 30 | return; 31 | } 32 | 33 | // See https://doc.replicache.dev/licensing for how to get a license key. 34 | const licenseKey = TEST_LICENSE_KEY; 35 | if (!licenseKey) { 36 | throw new Error("Missing VITE_REPLICACHE_LICENSE_KEY"); 37 | } 38 | 39 | const r = new Replicache({ 40 | licenseKey, 41 | pushURL: `/api/replicache/push?spaceID=${listID}`, 42 | pullURL: `/api/replicache/pull?spaceID=${listID}`, 43 | name: listID, 44 | mutators, 45 | }); 46 | 47 | // Implements a Replicache poke using Server-Sent Events. 48 | // If a "poke" message is received, it will pull from the server. 49 | const ev = new EventSource(`/api/replicache/poke?spaceID=${listID}`, { 50 | withCredentials: true, 51 | }); 52 | ev.onmessage = async (event) => { 53 | if (event.data === "poke") { 54 | await r.pull(); 55 | } 56 | }; 57 | 58 | ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( 59 | 60 | 61 | , 62 | ); 63 | } 64 | await init(); 65 | -------------------------------------------------------------------------------- /packages/react-native-expo-sqlite/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@react-native-replicache/react-native-expo-sqlite", 3 | "version": "1.3.1", 4 | "main": "./dist/commonjs/index.js", 5 | "module": "./dist/module/index.js", 6 | "react-native": "./src/index.ts", 7 | "types": "./dist/typescript/index.d.ts", 8 | "files": [ 9 | "dist/", 10 | "src/" 11 | ], 12 | "type": "module", 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/braden1996/react-native-replicache.git", 16 | "directory": "packages/react-native-expo-sqlite" 17 | }, 18 | "keywords": [ 19 | "react-native-replicache", 20 | "braden1996" 21 | ], 22 | "author": "Braden Marshall ", 23 | "license": "MIT", 24 | "homepage": "https://github.com/braden1996/react-native-replicache", 25 | "bugs": { 26 | "url": "https://github.com/braden1996/react-native-replicache/issues" 27 | }, 28 | "scripts": { 29 | "build": "bob build", 30 | "test": "yarn run test-typescript", 31 | "test-typescript": "yarn run root:tsc --noEmit", 32 | "lint": "yarn run root:eslint ./src --ext .js,.ts,.tsx", 33 | "lint-fix": "yarn run lint --fix", 34 | "clean": "yarn run root:rimraf dist .turbo" 35 | }, 36 | "dependencies": { 37 | "@react-native-replicache/replicache-generic-sqlite": "1.3.0" 38 | }, 39 | "devDependencies": { 40 | "@braden1996/tsconfig": "^0.0.1", 41 | "@types/react": "~18.2.79", 42 | "react-native-builder-bob": "^0.20.3" 43 | }, 44 | "peerDependencies": { 45 | "expo-sqlite": ">=14.0.0", 46 | "react-native": ">=0.74.0" 47 | }, 48 | "react-native-builder-bob": { 49 | "source": "src", 50 | "output": "dist", 51 | "targets": [ 52 | "commonjs", 53 | "module", 54 | [ 55 | "typescript", 56 | { 57 | "tsc": "../../node_modules/.bin/tsc" 58 | } 59 | ] 60 | ] 61 | }, 62 | "eslintConfig": { 63 | "extends": "universe/native" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /packages/react-native-op-sqlite/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@react-native-replicache/react-native-op-sqlite", 3 | "version": "1.3.1", 4 | "main": "./dist/commonjs/index.js", 5 | "module": "./dist/module/index.js", 6 | "react-native": "./src/index.ts", 7 | "types": "./dist/typescript/index.d.ts", 8 | "files": [ 9 | "dist/", 10 | "src/" 11 | ], 12 | "type": "module", 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/braden1996/react-native-replicache.git", 16 | "directory": "packages/react-native-op-sqlite" 17 | }, 18 | "keywords": [ 19 | "react-native-replicache", 20 | "braden1996" 21 | ], 22 | "author": "Braden Marshall ", 23 | "license": "MIT", 24 | "homepage": "https://github.com/braden1996/react-native-replicache", 25 | "bugs": { 26 | "url": "https://github.com/braden1996/react-native-replicache/issues" 27 | }, 28 | "scripts": { 29 | "build": "bob build", 30 | "test": "yarn run test-typescript", 31 | "test-typescript": "yarn run root:tsc --noEmit", 32 | "lint": "yarn run root:eslint ./src --ext .js,.ts,.tsx", 33 | "lint-fix": "yarn run lint --fix", 34 | "clean": "yarn run root:rimraf dist .turbo" 35 | }, 36 | "dependencies": { 37 | "@react-native-replicache/replicache-generic-sqlite": "1.3.0" 38 | }, 39 | "devDependencies": { 40 | "@babel/preset-env": "^7.20.0", 41 | "@babel/runtime": "^7.20.0", 42 | "@braden1996/tsconfig": "^0.0.1", 43 | "@types/react": "~18.2.79", 44 | "react-native-builder-bob": "^0.20.3" 45 | }, 46 | "peerDependencies": { 47 | "@op-engineering/op-sqlite": "^6.0.1" 48 | }, 49 | "react-native-builder-bob": { 50 | "source": "src", 51 | "output": "dist", 52 | "targets": [ 53 | "commonjs", 54 | "module", 55 | [ 56 | "typescript", 57 | { 58 | "tsc": "../../node_modules/.bin/tsc" 59 | } 60 | ] 61 | ] 62 | }, 63 | "eslintConfig": { 64 | "extends": "universe/native" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /packages/example/web-react/vite.config.ts.timestamp-1676753335795.mjs: -------------------------------------------------------------------------------- 1 | // vite.config.ts 2 | import { defineConfig } from "file:///Users/braden/Development/react-native-replicache/node_modules/vite/dist/node/index.js"; 3 | import react from "file:///Users/braden/Development/react-native-replicache/node_modules/@vitejs/plugin-react/dist/index.mjs"; 4 | var vite_config_default = defineConfig({ 5 | plugins: [react()], 6 | build: { 7 | target: "esnext" 8 | }, 9 | server: { 10 | proxy: { 11 | "/api": { 12 | target: "http://127.0.0.1:8080" 13 | } 14 | } 15 | } 16 | }); 17 | export { 18 | vite_config_default as default 19 | }; 20 | //# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvVXNlcnMvYnJhZGVuL0RldmVsb3BtZW50L3JlYWN0LW5hdGl2ZS1yZXBsaWNhY2hlL3BhY2thZ2VzL2V4YW1wbGUvd2ViLXJlYWN0XCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ZpbGVuYW1lID0gXCIvVXNlcnMvYnJhZGVuL0RldmVsb3BtZW50L3JlYWN0LW5hdGl2ZS1yZXBsaWNhY2hlL3BhY2thZ2VzL2V4YW1wbGUvd2ViLXJlYWN0L3ZpdGUuY29uZmlnLnRzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ltcG9ydF9tZXRhX3VybCA9IFwiZmlsZTovLy9Vc2Vycy9icmFkZW4vRGV2ZWxvcG1lbnQvcmVhY3QtbmF0aXZlLXJlcGxpY2FjaGUvcGFja2FnZXMvZXhhbXBsZS93ZWItcmVhY3Qvdml0ZS5jb25maWcudHNcIjtpbXBvcnQge2RlZmluZUNvbmZpZ30gZnJvbSAndml0ZSc7XG5pbXBvcnQgcmVhY3QgZnJvbSAnQHZpdGVqcy9wbHVnaW4tcmVhY3QnO1xuXG4vLyBodHRwczovL3ZpdGVqcy5kZXYvY29uZmlnL1xuZXhwb3J0IGRlZmF1bHQgZGVmaW5lQ29uZmlnKHtcbiAgcGx1Z2luczogW3JlYWN0KCldLFxuICBidWlsZDoge1xuICAgIHRhcmdldDogJ2VzbmV4dCcsXG4gIH0sXG4gIHNlcnZlcjoge1xuICAgIHByb3h5OiB7XG4gICAgICAnL2FwaSc6IHtcbiAgICAgICAgdGFyZ2V0OiAnaHR0cDovLzEyNy4wLjAuMTo4MDgwJyxcbiAgICAgIH0sXG4gICAgfSxcbiAgfSxcbn0pO1xuIl0sCiAgIm1hcHBpbmdzIjogIjtBQUFzWixTQUFRLG9CQUFtQjtBQUNqYixPQUFPLFdBQVc7QUFHbEIsSUFBTyxzQkFBUSxhQUFhO0FBQUEsRUFDMUIsU0FBUyxDQUFDLE1BQU0sQ0FBQztBQUFBLEVBQ2pCLE9BQU87QUFBQSxJQUNMLFFBQVE7QUFBQSxFQUNWO0FBQUEsRUFDQSxRQUFRO0FBQUEsSUFDTixPQUFPO0FBQUEsTUFDTCxRQUFRO0FBQUEsUUFDTixRQUFRO0FBQUEsTUFDVjtBQUFBLElBQ0Y7QUFBQSxFQUNGO0FBQ0YsQ0FBQzsiLAogICJuYW1lcyI6IFtdCn0K 21 | -------------------------------------------------------------------------------- /packages/example/mobile-react-native/src/use-replicache.ts: -------------------------------------------------------------------------------- 1 | import { mutators } from "@react-native-replicache/example-shared"; 2 | // import { createReplicacheExpoSQLiteKVStore } from "@react-native-replicache/react-native-expo-sqlite"; 3 | import { createReplicacheReactNativeOPSQLiteKVStore } from "@react-native-replicache/react-native-op-sqlite"; 4 | import React from "react"; 5 | import EventSource from "react-native-sse"; 6 | import { Replicache, TEST_LICENSE_KEY, dropAllDatabases } from "replicache"; 7 | 8 | export function useReplicache(listID: string) { 9 | // See https://doc.replicache.dev/licensing for how to get a license key. 10 | const licenseKey = TEST_LICENSE_KEY; 11 | if (!licenseKey) { 12 | throw new Error("Missing VITE_REPLICACHE_LICENSE_KEY"); 13 | } 14 | 15 | const rep = React.useMemo( 16 | () => 17 | new Replicache({ 18 | licenseKey, 19 | pushURL: `http://127.0.0.1:8080/api/replicache/push?spaceID=${listID}`, 20 | pullURL: `http://127.0.0.1:8080/api/replicache/pull?spaceID=${listID}`, 21 | kvStore: createReplicacheReactNativeOPSQLiteKVStore, 22 | name: listID, 23 | mutators, 24 | }), 25 | [listID], 26 | ); 27 | 28 | const close = React.useCallback(async () => { 29 | await rep.close(); 30 | await dropAllDatabases({ 31 | kvStore: createReplicacheReactNativeOPSQLiteKVStore, 32 | }); 33 | }, []); 34 | 35 | React.useEffect(() => { 36 | // Note: React Native doesn't support SSE; this is just a polyfill. 37 | // You probably want to setup a WebSocket connection via Pusher. 38 | const ev = new EventSource( 39 | `http://127.0.0.1:8080/api/replicache/poke?spaceID=${listID}`, 40 | { 41 | headers: { 42 | withCredentials: true, 43 | }, 44 | }, 45 | ); 46 | 47 | ev.addEventListener("message", async (evt) => { 48 | if (evt.type !== "message") return; 49 | if (evt.data === "poke") { 50 | await rep.pull(); 51 | } 52 | }); 53 | 54 | return () => { 55 | ev.close(); 56 | }; 57 | }, [listID]); 58 | 59 | return { rep, close }; 60 | } 61 | -------------------------------------------------------------------------------- /packages/replicache-generic-sqlite/src/replicache-generic-sqlite-store.ts: -------------------------------------------------------------------------------- 1 | import type { KVStore } from "replicache"; 2 | 3 | import { ReplicacheGenericSQLiteDatabaseManager } from "./replicache-generic-sqlite-database-manager"; 4 | import { ReplicacheGenericSQLiteReadImpl } from "./replicache-generic-sqlite-read-impl"; 5 | import { ReplicacheGenericSQLiteWriteImpl } from "./replicache-generic-sqlite-write-impl"; 6 | 7 | export class ReplicacheGenericStore implements KVStore { 8 | private _closed = false; 9 | 10 | constructor( 11 | private readonly name: string, 12 | private readonly _dbm: ReplicacheGenericSQLiteDatabaseManager, 13 | ) {} 14 | 15 | async read() { 16 | const db = await this._getDb(); 17 | const tx = db.transaction(); 18 | await tx.start(true); 19 | return new ReplicacheGenericSQLiteReadImpl(tx); 20 | } 21 | 22 | async withRead( 23 | fn: (read: Awaited>) => R | Promise, 24 | ): Promise { 25 | const read = await this.read(); 26 | try { 27 | return await fn(read); 28 | } finally { 29 | read.release(); 30 | } 31 | } 32 | 33 | async write(): Promise>> { 34 | const db = await this._getDb(); 35 | const tx = db.transaction(); 36 | await tx.start(false); 37 | return new ReplicacheGenericSQLiteWriteImpl(tx); 38 | } 39 | 40 | async withWrite( 41 | fn: (write: Awaited>) => R | Promise, 42 | ): Promise { 43 | const write = await this.write(); 44 | try { 45 | return await fn(write); 46 | } finally { 47 | write.release(); 48 | } 49 | } 50 | 51 | async close() { 52 | await this._dbm.close(this.name); 53 | this._closed = true; 54 | } 55 | 56 | get closed(): boolean { 57 | return this._closed; 58 | } 59 | 60 | private async _getDb() { 61 | return await this._dbm.open(this.name); 62 | } 63 | } 64 | 65 | export function getCreateReplicacheSQLiteKVStore( 66 | db: ReplicacheGenericSQLiteDatabaseManager, 67 | ) { 68 | return (name: string) => new ReplicacheGenericStore(name, db); 69 | } 70 | -------------------------------------------------------------------------------- /packages/example/web-react/src/components/main-section.tsx: -------------------------------------------------------------------------------- 1 | import { Todo, TodoUpdate } from "@react-native-replicache/example-shared"; 2 | import { useState } from "react"; 3 | 4 | import Footer from "./footer"; 5 | import TodoList from "./todo-list"; 6 | 7 | const MainSection = ({ 8 | todos, 9 | onUpdateTodo, 10 | onDeleteTodos, 11 | onCompleteTodos, 12 | }: { 13 | todos: Todo[]; 14 | onUpdateTodo: (update: TodoUpdate) => void; 15 | onDeleteTodos: (ids: string[]) => void; 16 | onCompleteTodos: (completed: boolean, ids: string[]) => void; 17 | }) => { 18 | const todosCount = todos.length; 19 | const completed = todos.filter((todo) => todo.completed); 20 | const completedCount = completed.length; 21 | const toggleAllValue = completedCount === todosCount; 22 | 23 | const [filter, setFilter] = useState("All"); 24 | 25 | const filteredTodos = todos.filter((todo) => { 26 | if (filter === "All") { 27 | return true; 28 | } 29 | if (filter === "Active") { 30 | return !todo.completed; 31 | } 32 | if (filter === "Completed") { 33 | return todo.completed; 34 | } 35 | throw new Error("Unknown filter: " + filter); 36 | }); 37 | 38 | const handleCompleteAll = () => { 39 | const completed = !toggleAllValue; 40 | onCompleteTodos( 41 | completed, 42 | todos.map((todo) => todo.id), 43 | ); 44 | }; 45 | 46 | return ( 47 |
    48 | {todosCount > 0 && ( 49 | 50 | 55 | 57 | )} 58 | onDeleteTodos([id])} 62 | /> 63 | {todos.length > 0 && ( 64 |
    68 | onDeleteTodos(completed.map((todo) => todo.id)) 69 | } 70 | currentFilter={filter} 71 | onFilter={setFilter} 72 | /> 73 | )} 74 |
    75 | ); 76 | }; 77 | 78 | export default MainSection; 79 | -------------------------------------------------------------------------------- /packages/example/mobile-react-native/src/components/todo-item.tsx: -------------------------------------------------------------------------------- 1 | import { Todo, TodoUpdate } from "@react-native-replicache/example-shared"; 2 | import { Pressable, StyleSheet, Text, View } from "react-native"; 3 | 4 | interface TodoItemProps { 5 | todo: Todo; 6 | handleUpdate: (update: Omit) => void; 7 | handleDelete: () => void; 8 | } 9 | 10 | export function TodoItem({ todo, handleUpdate, handleDelete }: TodoItemProps) { 11 | return ( 12 | 13 | handleUpdate({ completed: !todo.completed })}> 14 | {todo.completed ? ( 15 | 16 | 17 | 18 | ) : ( 19 | 20 | )} 21 | 22 | 31 | {todo.text} 32 | 33 | 34 | handleDelete()}> 35 | 36 | X 37 | 38 | 39 | 40 | ); 41 | } 42 | 43 | const styles = StyleSheet.create({ 44 | checkbox: { 45 | width: 40, 46 | height: 40, 47 | borderWidth: 1, 48 | borderColor: "#a9a9a9", 49 | borderRadius: 20, 50 | marginRight: 16, 51 | justifyContent: "center", 52 | alignItems: "center", 53 | }, 54 | checkboxCompleted: { 55 | borderColor: "#3fa390", 56 | }, 57 | checkboxCompletedCheckText: { 58 | color: "#3fa390", 59 | fontSize: 24, 60 | }, 61 | itemContainer: { 62 | paddingHorizontal: 8, 63 | paddingVertical: 12, 64 | flexDirection: "row", 65 | alignItems: "center", 66 | backgroundColor: "white", 67 | }, 68 | itemText: { 69 | flex: 1, 70 | fontSize: 24, 71 | lineHeight: 1.2 * 24, 72 | fontWeight: "400", 73 | color: "#484848", 74 | }, 75 | deleteContainer: { 76 | marginLeft: 16, 77 | width: 40, 78 | height: 40, 79 | justifyContent: "center", 80 | alignItems: "center", 81 | }, 82 | deleteText: { 83 | color: "#c94545", 84 | fontSize: 24, 85 | }, 86 | itemTextCompleted: { 87 | textDecorationLine: "line-through", 88 | color: "#949494", 89 | }, 90 | }); 91 | -------------------------------------------------------------------------------- /packages/example/mobile-react-native/src/components/todo-list.tsx: -------------------------------------------------------------------------------- 1 | import { listTodos, TodoUpdate } from "@react-native-replicache/example-shared"; 2 | import { nanoid } from "nanoid"; 3 | import React from "react"; 4 | import { Button, FlatList, StyleSheet, Text, View } from "react-native"; 5 | import { useSubscribe } from "replicache-react"; 6 | 7 | import { TodoInput } from "./todo-input"; 8 | import { TodoItem } from "./todo-item"; 9 | import { useReplicache } from "../use-replicache"; 10 | 11 | interface TodoListProps { 12 | listId: string; 13 | } 14 | 15 | export function TodoList({ listId }: TodoListProps) { 16 | const { rep, close } = useReplicache(listId); 17 | 18 | // Subscribe to all todos and sort them. 19 | const todos = useSubscribe(rep, listTodos, [], [rep]); 20 | todos.sort((a, b) => a.sort - b.sort); 21 | 22 | // Define event handlers and connect them to Replicache mutators. Each 23 | // of these mutators runs immediately (optimistically) locally, then runs 24 | // again on the server-side automatically. 25 | const handleNewItem = (text: string) => { 26 | rep.mutate.createTodo({ 27 | id: nanoid(), 28 | text, 29 | completed: false, 30 | }); 31 | }; 32 | 33 | const handleUpdateTodo = (update: TodoUpdate) => 34 | rep.mutate.updateTodo(update); 35 | 36 | const handleDeleteTodos = async (ids: string[]) => { 37 | for (const id of ids) { 38 | await rep.mutate.deleteTodo(id); 39 | } 40 | }; 41 | 42 | return ( 43 | ( 46 | handleDeleteTodos([item.id])} 49 | handleUpdate={(update) => 50 | handleUpdateTodo({ id: item.id, ...update }) 51 | } 52 | /> 53 | )} 54 | ListHeaderComponent={} 55 | ListFooterComponent={ 56 | 57 | List: {listId} 58 | 59 |