├── .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 |
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 |
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 |
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 |
60 |
61 | }
62 | ItemSeparatorComponent={() => }
63 | style={{ flex: 1 }}
64 | contentContainerStyle={{ paddingHorizontal: 32 }}
65 | />
66 | );
67 | }
68 |
69 | const styles = StyleSheet.create({
70 | separator: {
71 | width: "100%",
72 | height: StyleSheet.hairlineWidth,
73 | backgroundColor: "#e6e6e6",
74 | },
75 | footerContainer: {
76 | padding: 8,
77 | flexDirection: "row",
78 | justifyContent: "center",
79 | backgroundColor: "#d9d9d9",
80 | borderBottomLeftRadius: 8,
81 | borderBottomRightRadius: 8,
82 | },
83 | footerText: {
84 | color: "#111111",
85 | fontSize: 15,
86 | lineHeight: 19,
87 | },
88 | });
89 |
--------------------------------------------------------------------------------
/packages/react-native-expo-sqlite/README.md:
--------------------------------------------------------------------------------
1 | # React Native Replicache
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 | Replicache enables us to build applications that are performant, offline-capable and collaborative. By default, it uses [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) for client-side persistance. Unfortunately, this technology is not available in React Native and is only supported in web-browsers.
15 |
16 | Thankfully, Replicache allows us to provide our own transactional data-store via [`kvStore`](https://doc.replicache.dev/api/interfaces/ReplicacheOptions#kvstoree). The goal of this project is to provide some implementations of such a store, along with some guidance in getting up and running with Replicache in React Native.
17 |
18 | ## What are the strategies?
19 |
20 | React Native has relatively good support for SQLite - which provides the [strict serializable](https://jepsen.io/consistency/models/strict-serializable) transactions that we require.
21 |
22 | Here we provide a store implementation backed by [`expo-sqlite`](https://docs.expo.dev/versions/latest/sdk/sqlite/). However, we also offer [more bindings here](https://github.com/Braden1996/react-native-replicache). Be sure to see what best fits your project!
23 |
24 | ### Any additional considerations?
25 |
26 | Some configuration is required to receive [poke](https://doc.replicache.dev/byob/poke) events from the server. In our example, [seen here](https://github.com/Braden1996/react-native-replicache/blob/master/packages/example/mobile-react-native/src/use-replicache.ts), we use a polyfill for Server Sent Events. These aren't built into React Native, but are really handy for a demo.
27 |
28 | You most likely want to use web-sockets for this. This is relatively trivial with Pusher/Ably etc and similar to the web-app so we won't discuss that further here.
29 |
30 | ## How can I install this?
31 |
32 | 1. Install the following in your React Native project:
33 | - `yarn add expo-crypto expo-sqlite @react-native-replicache/react-native-expo-sqlite`
34 | 2. Ensure that you've polyfilled `crypto.getRandomValues` on the global namespace.
35 | - See [here for an example](https://github.com/Braden1996/react-native-replicache/blob/master/packages/example/mobile-react-native/src/crypto-polyfill.ts).
36 | 3. Pass in `createReplicacheExpoSQLiteKVStore` to Replicache's `kvStore` option.
37 | - See [here for an example](https://github.com/Braden1996/react-native-replicache/blob/master/packages/example/mobile-react-native/src/use-replicache.ts).
38 |
--------------------------------------------------------------------------------
/packages/react-native-op-sqlite/README.md:
--------------------------------------------------------------------------------
1 | # React Native Replicache
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 | Replicache enables us to build applications that are performant, offline-capable and collaborative. By default, it uses [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) for client-side persistance. Unfortunately, this technology is not available in React Native and is only supported in web-browsers.
15 |
16 | Thankfully, Replicache allows us to provide our own transactional data-store via [`kvStore`](https://doc.replicache.dev/api/interfaces/ReplicacheOptions#kvstoree). The goal of this project is to provide some implementations of such a store, along with some guidance in getting up and running with Replicache in React Native.
17 |
18 | ## What are the strategies?
19 |
20 | React Native has relatively good support for SQLite - which provides the [strict serializable](https://jepsen.io/consistency/models/strict-serializable) transactions that we require.
21 |
22 | Here we provide a store implementation backed by [`op-sqlite`](https://github.com/OP-Engineering/op-sqlite). However, we also offer [more bindings here](https://github.com/Braden1996/react-native-replicache). Be sure to see what best fits your project!
23 |
24 | ### Any additional considerations?
25 |
26 | Some configuration is required to receive [poke](https://doc.replicache.dev/byob/poke) events from the server. In our example, [seen here](https://github.com/Braden1996/react-native-replicache/blob/master/packages/example/mobile-react-native/src/use-replicache.ts), we use a polyfill for Server Sent Events. These aren't built into React Native, but are really handy for a demo.
27 |
28 | You most likely want to use web-sockets for this. This is relatively trivial with Pusher/Ably etc and similar to the web-app so we won't discuss that further here.
29 |
30 | ## How can I install this?
31 |
32 | 1. Install the following in your React Native project:
33 | - `yarn add expo-crypto @op-engineering/op-sqlite @react-native-replicache/react-native-op-sqlite`
34 | 2. Ensure that you've polyfilled `crypto.getRandomValues` on the global namespace.
35 | - See [here for an example](https://github.com/Braden1996/react-native-replicache/blob/master/packages/example/mobile-react-native/src/crypto-polyfill.ts).
36 | 3. Pass in `createReplicacheOPSQLiteKVStore` to Replicache's `kvStore` option.
37 | - See [here for an example](https://github.com/Braden1996/react-native-replicache/blob/master/packages/example/mobile-react-native/src/use-replicache.ts).
38 |
--------------------------------------------------------------------------------
/packages/example/shared/src/mutators.ts:
--------------------------------------------------------------------------------
1 | // This file defines our "mutators".
2 | //
3 | // Mutators are how you change data in Replicache apps.
4 | //
5 | // They are registered with Replicache at construction-time and callable like:
6 | // `myReplicache.mutate.createTodo({text: "foo"})`.
7 | //
8 | // Replicache runs each mutation immediately (optimistically) on the client,
9 | // against the local cache, and then later (usually moments later) sends a
10 | // description of the mutation (its name and arguments) to the server, so that
11 | // the server can *re-run* the mutation there against the authoritative
12 | // datastore.
13 | //
14 | // This re-running of mutations is how Replicache handles conflicts: the
15 | // mutators defensively check the database when they run and do the appropriate
16 | // thing. The Replicache sync protocol ensures that the server-side result takes
17 | // precedence over the client-side optimistic result.
18 | //
19 | // If the server is written in JavaScript, the mutator functions can be directly
20 | // reused on the server. This sample demonstrates the pattern by using these
21 | // mutators both with Replicache on the client (see [id]].tsx) and on the server
22 | // (see pages/api/replicache/[op].ts).
23 | //
24 | // See https://doc.replicache.dev/how-it-works#sync-details for all the detail
25 | // on how Replicache syncs and resolves conflicts, but understanding that is not
26 | // required to get up and running.
27 |
28 | import type { WriteTransaction } from "replicache";
29 |
30 | import { Todo, listTodos, TodoUpdate } from "./todo";
31 |
32 | export type M = typeof mutators;
33 |
34 | export const mutators = {
35 | updateTodo: async (tx: WriteTransaction, update: TodoUpdate) => {
36 | // In a real app you may want to validate the incoming data is in fact a
37 | // TodoUpdate. Check out https://www.npmjs.com/package/@rocicorp/rails for
38 | // some heper functions to do this.
39 | const prev = (await tx.get(update.id)) as Todo;
40 | const next = { ...prev, ...update };
41 | await tx.put(next.id, next);
42 | },
43 |
44 | deleteTodo: async (tx: WriteTransaction, id: string) => {
45 | await tx.del(id);
46 | },
47 |
48 | // This mutator creates a new todo, assigning the next available sort value.
49 | //
50 | // If two clients create new todos concurrently, they both might choose the
51 | // same sort value locally (optimistically). That's fine because later when
52 | // the mutator re-runs on the server the two todos will get unique values.
53 | //
54 | // Replicache will automatically sync the change back to the clients,
55 | // reconcile any changes that happened client-side in the meantime, and update
56 | // the UI to reflect the changes.
57 | createTodo: async (tx: WriteTransaction, todo: Omit) => {
58 | const todos = await listTodos(tx);
59 | todos.sort((t1, t2) => t1.sort - t2.sort);
60 |
61 | const maxSort = todos.pop()?.sort ?? 0;
62 | await tx.put(todo.id, { ...todo, sort: maxSort + 1 });
63 | },
64 | };
65 |
--------------------------------------------------------------------------------
/packages/react-native-op-sqlite/src/replicache-op-sqlite-transaction.ts:
--------------------------------------------------------------------------------
1 | import * as OPSQLite from "@op-engineering/op-sqlite";
2 | import { ReplicacheGenericSQLiteTransaction } from "@react-native-replicache/replicache-generic-sqlite";
3 |
4 | export class ReplicacheOPSQLiteTransaction extends ReplicacheGenericSQLiteTransaction {
5 | private _tx: OPSQLite.Transaction | null = null;
6 | private _transactionCommittedSubscriptions = new Set<() => void>();
7 | private _txCommitted = false;
8 | private _transactionEndedSubscriptions = new Set<{
9 | resolve: () => void;
10 | reject: () => void;
11 | }>();
12 | private _txEnded = false;
13 |
14 | constructor(private readonly db: OPSQLite.OPSQLiteConnection) {
15 | super();
16 | }
17 |
18 | // op-sqlite doesn't support readonly
19 | public async start() {
20 | return await new Promise((resolve, reject) => {
21 | let didResolve = false;
22 | try {
23 | this.db.transaction(async (tx) => {
24 | didResolve = true;
25 | this._tx = tx;
26 | resolve();
27 |
28 | try {
29 | // op-sqlite auto-commits our transaction when this callback ends.
30 | // Lets artificially keep it open until we commit.
31 | await this._waitForTransactionCommitted();
32 | this._setTransactionEnded(false);
33 | } catch {
34 | this._setTransactionEnded(true);
35 | }
36 | });
37 | } catch {
38 | if (!didResolve) {
39 | reject(new Error("Did not resolve"));
40 | }
41 | }
42 | });
43 | }
44 |
45 | public async execute(
46 | sqlStatement: string,
47 | args?: (string | number | null)[] | undefined,
48 | ) {
49 | const tx = this.assertTransactionReady();
50 | const { rows } = tx.execute(sqlStatement, args);
51 |
52 | if (!rows || rows.length === 0) {
53 | return { item: () => undefined, length: 0 };
54 | }
55 |
56 | return {
57 | item: (idx: number) => rows.item(idx),
58 | length: rows.length,
59 | };
60 | }
61 |
62 | public async commit() {
63 | const tx = this.assertTransactionReady();
64 | tx.commit();
65 | this._txCommitted = true;
66 | for (const resolver of this._transactionCommittedSubscriptions) {
67 | resolver();
68 | }
69 | this._transactionCommittedSubscriptions.clear();
70 | }
71 |
72 | public waitForTransactionEnded() {
73 | if (this._txEnded) return;
74 | return new Promise((resolve, reject) => {
75 | this._transactionEndedSubscriptions.add({ resolve, reject });
76 | });
77 | }
78 |
79 | private assertTransactionReady() {
80 | if (this._tx === null) throw new Error("Transaction is not ready.");
81 | if (this._txCommitted) throw new Error("Transaction already committed.");
82 | if (this._txEnded) throw new Error("Transaction already ended.");
83 | return this._tx;
84 | }
85 |
86 | private _waitForTransactionCommitted() {
87 | if (this._txCommitted) return;
88 | return new Promise((resolve) => {
89 | this._transactionCommittedSubscriptions.add(resolve);
90 | });
91 | }
92 |
93 | private _setTransactionEnded(errored = false) {
94 | this._txEnded = true;
95 | for (const { resolve, reject } of this._transactionEndedSubscriptions) {
96 | if (errored) {
97 | reject();
98 | } else {
99 | resolve();
100 | }
101 | }
102 | this._transactionEndedSubscriptions.clear();
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/packages/react-native-expo-sqlite/src/replicache-expo-sqlite-transaction.ts:
--------------------------------------------------------------------------------
1 | import { ReplicacheGenericSQLiteTransaction } from "@react-native-replicache/replicache-generic-sqlite";
2 | import * as SQLite from "expo-sqlite";
3 |
4 | export class ReplicacheExpoSQLiteTransaction extends ReplicacheGenericSQLiteTransaction {
5 | private _tx:
6 | | Parameters<
7 | Parameters[0]
8 | >[0]
9 | | null = null;
10 | private _transactionCommittedSubscriptions = new Set<() => void>();
11 | private _txCommitted = false;
12 | private _transactionEndedSubscriptions = new Set<{
13 | resolve: () => void;
14 | reject: () => void;
15 | }>();
16 | private _txEnded = false;
17 |
18 | constructor(private readonly db: SQLite.SQLiteDatabase) {
19 | super();
20 | }
21 |
22 | // expo-sqlite doesn't support readonly
23 | public async start() {
24 | return await new Promise((resolve, reject) => {
25 | let didResolve = false;
26 | try {
27 | this.db.withExclusiveTransactionAsync(async (tx) => {
28 | didResolve = true;
29 | this._tx = tx;
30 | resolve();
31 |
32 | try {
33 | // expo-sqlite auto-commits our transaction when this callback ends.
34 | // Lets artificially keep it open until we commit.
35 | await this._waitForTransactionCommitted();
36 | this._setTransactionEnded(false);
37 | } catch {
38 | this._setTransactionEnded(true);
39 | }
40 | });
41 | } catch {
42 | if (!didResolve) {
43 | reject(new Error("Did not resolve"));
44 | }
45 | }
46 | });
47 | }
48 |
49 | public async execute(
50 | sqlStatement: string,
51 | args?: (string | number | null)[] | undefined,
52 | ) {
53 | const tx = this.assertTransactionReady();
54 |
55 | const statement = await tx.prepareAsync(sqlStatement);
56 | let allRows: any;
57 | let result: any;
58 | try {
59 | result = await statement.executeAsync(...(args ?? []));
60 | allRows = await result.getAllAsync();
61 | } finally {
62 | await statement.finalizeAsync();
63 | }
64 |
65 | return {
66 | item: (idx: number) => allRows[idx],
67 | length: allRows.length,
68 | };
69 | }
70 |
71 | public async commit() {
72 | // Transaction is committed automatically.
73 | this._txCommitted = true;
74 | for (const resolver of this._transactionCommittedSubscriptions) {
75 | resolver();
76 | }
77 | this._transactionCommittedSubscriptions.clear();
78 | }
79 |
80 | public waitForTransactionEnded() {
81 | if (this._txEnded) return;
82 | return new Promise((resolve, reject) => {
83 | this._transactionEndedSubscriptions.add({ resolve, reject });
84 | });
85 | }
86 |
87 | private assertTransactionReady() {
88 | if (this._tx === null) throw new Error("Transaction is not ready.");
89 | if (this._txCommitted) throw new Error("Transaction already committed.");
90 | if (this._txEnded) throw new Error("Transaction already ended.");
91 | return this._tx;
92 | }
93 |
94 | private _waitForTransactionCommitted() {
95 | if (this._txCommitted) return;
96 | return new Promise((resolve) => {
97 | this._transactionCommittedSubscriptions.add(resolve);
98 | });
99 | }
100 |
101 | private _setTransactionEnded(errored = false) {
102 | this._txEnded = true;
103 | for (const { resolve, reject } of this._transactionEndedSubscriptions) {
104 | if (errored) {
105 | reject();
106 | } else {
107 | resolve();
108 | }
109 | }
110 | this._transactionEndedSubscriptions.clear();
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/packages/example/web-react/src/assets/react.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React Native Replicache
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 | Replicache enables us to build applications that are performant, offline-capable and collaborative. By default, it uses [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) for client-side persistance. Unfortunately, this technology is not available in React Native and is only supported in web-browsers.
15 |
16 | Thankfully, Replicache allows us to provide our own transactional data-store via [`kvStore`](https://doc.replicache.dev/api/interfaces/ReplicacheOptions#kvstoree). The goal of this project is to provide some implementations of such a store, along with some guidance in getting up and running with Replicache in React Native.
17 |
18 | ## What are the strategies?
19 |
20 | React Native has relatively good support for SQLite - which provides the [strict serializable](https://jepsen.io/consistency/models/strict-serializable) transactions that we require.
21 |
22 | In particular, we provide the choice between three SQLite bindings:
23 |
24 | 1. [`@react-native-replicache/react-native-expo-sqlite`](https://github.com/Braden1996/react-native-replicache/tree/master/packages/react-native-expo-sqlite)
25 | - Backed by [`expo-sqlite`](https://docs.expo.dev/versions/latest/sdk/sqlite/)
26 | - Supported in [Expo Go](https://expo.dev/client).
27 | 2. [`@react-native-replicache/react-native-op-sqlite`](https://github.com/Braden1996/react-native-replicache/tree/master/packages/react-native-op-sqlite)
28 | - Backed by [`react-native-op-sqlite`](https://github.com/OP-Engineering/op-sqlite)
29 | - Better performance.
30 |
31 | ### Any additional considerations?
32 |
33 | Some configuration is required to receive [poke](https://doc.replicache.dev/byob/poke) events from the server. In our example, [seen here](https://github.com/Braden1996/react-native-replicache/blob/master/packages/example/mobile-react-native/src/use-replicache.ts), we use a polyfill for Server Sent Events. These aren't built into React Native, but are really handy for a demo.
34 |
35 | You most likely want to use web-sockets for this. This is relatively trivial with Pusher/Ably etc and similar to the web-app so we won't discuss that further here.
36 |
37 | ## How can I install this?
38 |
39 | 1. Install the following in your React Native project:
40 | - `yarn add expo-crypto`
41 | - Decide which SQLite binding is for you and install one of the following:
42 | - `yarn add @op-engineering/op-sqlite @react-native-replicache/react-native-op-sqlite`
43 | - `yarn add expo-sqlite @react-native-replicache/expo-sqlite`
44 | 2. Ensure that you've polyfilled `crypto.getRandomValues` on the global namespace.
45 | - See [here for an example](https://github.com/Braden1996/react-native-replicache/blob/master/packages/example/mobile-react-native/src/crypto-polyfill.ts).
46 | 3. Pass in your chosen SQLite binding's React Native Replicache binding into Replicache's `kvStore` option.
47 | - This will be one of the following, depending on the binding you chose:
48 | - `createReplicacheOPSQLiteKVStore`
49 | - `createReplicacheExpoSQLiteKVStore`
50 | - See [here for an example](https://github.com/Braden1996/react-native-replicache/blob/master/packages/example/mobile-react-native/src/use-replicache.ts).
51 |
52 | ## How can I experiment with this locally?
53 |
54 | ### Prerequisites
55 |
56 | - Environment capable of developing iOS/Android applications (iOS is likely preferred).
57 | - See [How to install React Native on Mac](https://dev-yakuza.posstree.com/en/react-native/install-on-mac/)
58 | - or: [Setting up the development environment](https://reactnative.dev/docs/environment-setup)
59 | - Note: Installing [Xcode](https://developer.apple.com/xcode/) from the [Mac App Store](https://apps.apple.com/us/app/xcode/id497799835?mt=12) tends to be unusually slow and buggy.
60 | - Try download it from the [Apple website](https://developer.apple.com/xcode/) instead.
61 |
62 | ### Instructions
63 |
64 | 1. Clone the repository: `git clone https://github.com/braden1996/react-native-replicache.git`
65 | 2. Install yarn dependencies from repo root: `yarn install`
66 | 3. Perform an initial build: `yarn build`
67 | 4. Install the example iOS app onto a simulator/emulator or connected physical device, e.g: `yarn workspace @react-native-replicache/example-mobile-react-native ios`
68 | 5. Once the above has installed onto your device, you can cancel the now running [Metro bundler](https://facebook.github.io/metro/) and simply start dev for all workspaces: `yarn run dev`.
69 |
70 | ### Tips
71 |
72 | - [Flipper](https://fbflipper.com/) has been configured for use with the example app.
73 | - Download it to browser network requests etc
74 |
--------------------------------------------------------------------------------
/packages/deep-freeze/src/deep-freeze.ts:
--------------------------------------------------------------------------------
1 | import type { ReadonlyJSONValue, ReadonlyJSONObject } from "replicache";
2 |
3 | import { throwInvalidType } from "./asserts";
4 | import { skipAssertJSONValue, skipFreeze, skipFrozenAsserts } from "./config";
5 | import type { Cookie, FrozenCookie } from "./cookies";
6 | import type { FrozenJSONValue } from "./frozen";
7 | import { hasOwn } from "./hasOwn";
8 |
9 | /**
10 | * We tag deep frozen objects in debug mode so that we do not have to deep
11 | * freeze an object more than once.
12 | */
13 | const deepFrozenObjects = new WeakSet