|
| 1 | +# The SQLSync Guide |
| 2 | + |
| 3 | +> [!IMPORTANT] SQLSync is in active development and thus is changing quickly. |
| 4 | +> Currently, do not use it in a production application as there is no backwards |
| 5 | +> compatibility or stability promise. |
| 6 | +
|
| 7 | +SQLSync is distributed as a Javascript package as well as a Rust Crate. |
| 8 | +Currently both are required to use SQLSync. Also, React is the only supported |
| 9 | +framework at the moment. |
| 10 | + |
| 11 | +If you want to jump ahead to a working demo, check out the finished product at: |
| 12 | +https://github.com/orbitinghail/sqlsync-demo-guestbook |
| 13 | + |
| 14 | +## Step 1: Creating the Reducer |
| 15 | + |
| 16 | +SQLSync requires that all mutations are handled by a piece of code called "The |
| 17 | +Reducer". Currently this code has to be written in Rust, however we have plans |
| 18 | +to make it possible to write Reducers using JS or other languages. The fastest |
| 19 | +way to create a reducer is to initialize a new Rust project like so: |
| 20 | + |
| 21 | +1. Make sure you have Rust installed; if not install using [rustup]. |
| 22 | +2. Install support for the `wasm32-unknown-unknown` target: |
| 23 | + |
| 24 | +```bash |
| 25 | +rustup target add wasm32-unknown-unknown |
| 26 | +``` |
| 27 | + |
| 28 | +3. Initialize the reducer: (feel free to rename) |
| 29 | + |
| 30 | +```bash |
| 31 | +cargo init --lib reducer |
| 32 | +cd reducer |
| 33 | +``` |
| 34 | + |
| 35 | +4. Update `Cargo.toml` to look something like this |
| 36 | + |
| 37 | +```toml |
| 38 | +[package] |
| 39 | +name = "reducer" |
| 40 | +version = "0.1.0" |
| 41 | +edition = "2021" |
| 42 | + |
| 43 | +[lib] |
| 44 | +crate-type = ["cdylib"] |
| 45 | + |
| 46 | +[profile.release] |
| 47 | +lto = true |
| 48 | +strip = "debuginfo" |
| 49 | +codegen-units = 1 |
| 50 | + |
| 51 | +[dependencies] |
| 52 | +sqlsync-reducer = "0.1" |
| 53 | +serde = { version = "1.0", features = ["derive"] } |
| 54 | +serde_json = "1.0" |
| 55 | +log = "0.4" |
| 56 | +``` |
| 57 | + |
| 58 | +5. Update `src/lib.rs` to look something like this: |
| 59 | + |
| 60 | +```rust |
| 61 | +use serde::Deserialize; |
| 62 | +use sqlsync_reducer::{execute, init_reducer, types::ReducerError}; |
| 63 | + |
| 64 | +#[derive(Deserialize, Debug)] |
| 65 | +#[serde(tag = "tag")] |
| 66 | +enum Mutation { |
| 67 | + InitSchema, |
| 68 | + AddMessage { id: String, msg: String }, |
| 69 | +} |
| 70 | + |
| 71 | +init_reducer!(reducer); |
| 72 | +async fn reducer(mutation: Vec<u8>) -> Result<(), ReducerError> { |
| 73 | + let mutation: Mutation = serde_json::from_slice(&mutation[..])?; |
| 74 | + |
| 75 | + match mutation { |
| 76 | + Mutation::InitSchema => { |
| 77 | + execute!( |
| 78 | + "CREATE TABLE IF NOT EXISTS messages ( |
| 79 | + id TEXT PRIMARY KEY, |
| 80 | + msg TEXT NOT NULL, |
| 81 | + created_at TEXT NOT NULL |
| 82 | + )" |
| 83 | + ).await; |
| 84 | + } |
| 85 | + |
| 86 | + Mutation::AddMessage { id, msg } => { |
| 87 | + log::info!("appending message({}): {}", id, msg); |
| 88 | + execute!( |
| 89 | + "insert into messages (id, msg, created_at) |
| 90 | + values (?, ?, datetime('now'))", |
| 91 | + id, msg |
| 92 | + ).await; |
| 93 | + } |
| 94 | + } |
| 95 | + |
| 96 | + Ok(()) |
| 97 | +} |
| 98 | +``` |
| 99 | + |
| 100 | +6. Compile your reducer to Wasm |
| 101 | + |
| 102 | +```bash |
| 103 | +cargo build --target wasm32-unknown-unknown --release |
| 104 | +``` |
| 105 | + |
| 106 | +## Step 2: Install and configure the React library |
| 107 | + |
| 108 | +```bash |
| 109 | +npm install @orbitinghail/sqlsync-react @orbitinghail/sqlsync-worker |
| 110 | +``` |
| 111 | + |
| 112 | +The following examples will be using Typescript to make everything a bit more |
| 113 | +precise. If you are not using Typescript you can still use SQLSync, just skip |
| 114 | +the type descriptions and annotations. |
| 115 | + |
| 116 | +Also, make sure your JS bundling tool supports importing assets from the |
| 117 | +filesystem, as will need that to easily get access to the Reducer we compiled |
| 118 | +earlier in this guide. If in doubt, [Vite] is highly recommended. |
| 119 | + |
| 120 | +Create a file which will contain type information for your Mutations, the |
| 121 | +reducer URL, and export some useful React hooks for your app to consume. It |
| 122 | +should look something like this: |
| 123 | + |
| 124 | +```typescript |
| 125 | +import { |
| 126 | + DocType, |
| 127 | + createDocHooks, |
| 128 | + serializeMutationAsJSON, |
| 129 | +} from "@orbitinghail/sqlsync-react"; |
| 130 | + |
| 131 | +// Path to your compiled reducer artifact, your js bundler should handle making |
| 132 | +// this a working URL that resolves during development and in production. |
| 133 | +const REDUCER_URL = new URL( |
| 134 | + "../reducer/target/wasm32-unknown-unknown/release/reducer.wasm", |
| 135 | + import.meta.url |
| 136 | +); |
| 137 | + |
| 138 | +// Must match the Mutation type in the Rust Reducer code |
| 139 | +export type Mutation = |
| 140 | + | { |
| 141 | + tag: "InitSchema"; |
| 142 | + } |
| 143 | + | { |
| 144 | + tag: "AddMessage"; |
| 145 | + id: string; |
| 146 | + msg: string; |
| 147 | + }; |
| 148 | + |
| 149 | +export const TaskDocType: DocType<Mutation> = { |
| 150 | + reducerUrl: REDUCER_URL, |
| 151 | + serializeMutation: serializeMutationAsJSON, |
| 152 | +}; |
| 153 | + |
| 154 | +export const { useMutate, useQuery, useSetConnectionEnabled } = |
| 155 | + createDocHooks(TaskDocType); |
| 156 | +``` |
| 157 | + |
| 158 | +## Step 3: Hooking it up to your app |
| 159 | + |
| 160 | +Using the hooks exported from the file in |
| 161 | +[Step 2](#step-2-install-and-configure-the-react-library) we can easily hook |
| 162 | +SQLSync up to our application. |
| 163 | + |
| 164 | +Here is a complete example of a very trivial guestbook application which uses |
| 165 | +the reducer we created above. If |
| 166 | + |
| 167 | +```tsx |
| 168 | +import React, { FormEvent, useEffect } from "react"; |
| 169 | +import ReactDOM from "react-dom/client"; |
| 170 | + |
| 171 | +// this example uses the uuid library (`npm install uuid`) |
| 172 | +import { v4 as uuidv4 } from "uuid"; |
| 173 | + |
| 174 | +// You'll need to configure your build system to make these entrypoints |
| 175 | +// available as urls. Vite does this automatically via the `?url` suffix. |
| 176 | +import sqlSyncWasmUrl from "@orbitinghail/sqlsync-worker/sqlsync.wasm?url"; |
| 177 | +import workerUrl from "@orbitinghail/sqlsync-worker/worker.js?url"; |
| 178 | + |
| 179 | +// import the SQLSync provider and hooks |
| 180 | +import { SQLSyncProvider, sql } from "@orbitinghail/sqlsync-react"; |
| 181 | +import { useMutate, useQuery } from "./doctype"; |
| 182 | + |
| 183 | +// Create a DOC_ID to use, each DOC_ID will correspond to a different SQLite |
| 184 | +// database. We use a static doc id so we can play with cross-tab sync. |
| 185 | +import { journalIdFromString } from "@orbitinghail/sqlsync-worker"; |
| 186 | +const DOC_ID = journalIdFromString("VM7fC4gKxa52pbdtrgd9G9"); |
| 187 | + |
| 188 | +// Configure the SQLSync provider near the top of the React tree |
| 189 | +ReactDOM.createRoot(document.getElementById("root")!).render( |
| 190 | + <SQLSyncProvider wasmUrl={sqlSyncWasmUrl} workerUrl={workerUrl}> |
| 191 | + <App /> |
| 192 | + </SQLSyncProvider> |
| 193 | +); |
| 194 | + |
| 195 | +// Use SQLSync hooks in your app |
| 196 | +export function App() { |
| 197 | + // we will use the standard useState hook to handle the message input box |
| 198 | + const [msg, setMsg] = React.useState(""); |
| 199 | + |
| 200 | + // create a mutate function for our document |
| 201 | + const mutate = useMutate(DOC_ID); |
| 202 | + |
| 203 | + // initialize the schema; eventually this will be handled by SQLSync automatically |
| 204 | + useEffect(() => { |
| 205 | + mutate({ tag: "InitSchema" }).catch((err) => { |
| 206 | + console.error("Failed to init schema", err); |
| 207 | + }); |
| 208 | + }, [mutate]); |
| 209 | + |
| 210 | + // create a callback which knows how to trigger the add message mutation |
| 211 | + const handleSubmit = React.useCallback( |
| 212 | + (e: FormEvent<HTMLFormElement>) => { |
| 213 | + // Prevent the browser from reloading the page |
| 214 | + e.preventDefault(); |
| 215 | + |
| 216 | + // create a unique message id |
| 217 | + const id = crypto.randomUUID ? crypto.randomUUID() : uuidv4(); |
| 218 | + |
| 219 | + // don't add empty messages |
| 220 | + if (msg.trim() !== "") { |
| 221 | + mutate({ tag: "AddMessage", id, msg }).catch((err) => { |
| 222 | + console.error("Failed to add message", err); |
| 223 | + }); |
| 224 | + // clear the message |
| 225 | + setMsg(""); |
| 226 | + } |
| 227 | + }, |
| 228 | + [mutate, msg] |
| 229 | + ); |
| 230 | + |
| 231 | + // finally, query SQLSync for all the messages, sorted by created_at |
| 232 | + const { rows } = useQuery<{ id: string; msg: string }>( |
| 233 | + DOC_ID, |
| 234 | + sql` |
| 235 | + select id, msg from messages |
| 236 | + order by created_at |
| 237 | + ` |
| 238 | + ); |
| 239 | + |
| 240 | + return ( |
| 241 | + <div> |
| 242 | + <h1>Guestbook:</h1> |
| 243 | + <ul> |
| 244 | + {(rows ?? []).map(({ id, msg }) => ( |
| 245 | + <li key={id}>{msg}</li> |
| 246 | + ))} |
| 247 | + </ul> |
| 248 | + <h3>Leave a message:</h3> |
| 249 | + <form onSubmit={handleSubmit}> |
| 250 | + <label> |
| 251 | + Msg: |
| 252 | + <input |
| 253 | + type="text" |
| 254 | + name="msg" |
| 255 | + value={msg} |
| 256 | + onChange={(e) => setMsg(e.target.value)} |
| 257 | + /> |
| 258 | + </label> |
| 259 | + <input type="submit" value="Submit" /> |
| 260 | + </form> |
| 261 | + </div> |
| 262 | + ); |
| 263 | +} |
| 264 | +``` |
| 265 | + |
| 266 | +## Step 4: Connect to the coordinator (COMING SOON) |
| 267 | + |
| 268 | +This step still requires using SQLSync from source. For now you'll have to |
| 269 | +follow the directions in the [Contribution Guide] to setup a Local Coordinator. |
| 270 | + |
| 271 | +[rustup]: https://rustup.rs/ |
| 272 | +[Vite]: https://vitejs.dev/ |
| 273 | +[Contribution Guide]: ./CONTRIBUTING.md |
0 commit comments