Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

solid js library #37

Merged
merged 22 commits into from
Jan 8, 2024
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions examples/guestbook-solid-js/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Solid JS + TypeScript + Vite + SQLSync

This template provides a minimal setup to get SQLSync working with Solid JS and Vite.
15 changes: 15 additions & 0 deletions examples/guestbook-solid-js/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<!doctype html>
<html lang="en">

<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>SQLSync + Vite + Solid JS + TS</title>
</head>

<body>
<div id="root">if nothing renders, open the console</div>
<script type="module" src="/src/main.tsx"></script>
</body>

</html>
24 changes: 24 additions & 0 deletions examples/guestbook-solid-js/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "guestbook-react",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview"
},
"dependencies": {
"@orbitinghail/sqlsync-solid-js": "workspace:*",
"@orbitinghail/sqlsync-worker": "workspace:*",
"uuid": "^9.0.1",
"solid-js": "^1.8.7"
},
"devDependencies": {
"@types/uuid": "^9.0.6",
"vite-plugin-solid": "^2.7.0",
"typescript": "^5.2.2",
"vite": "^4.5.0",
"vitest": "^0.34.6"
}
}
29 changes: 29 additions & 0 deletions examples/guestbook-solid-js/src/doctype.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { createDocHooks } from "@orbitinghail/sqlsync-solid-js";
import { DocType, serializeMutationAsJSON } from "@orbitinghail/sqlsync-worker";

const REDUCER_URL = new URL(
"../../../target/wasm32-unknown-unknown/release/reducer_guestbook.wasm",
import.meta.url
);

// Must match the Mutation type in the Rust Reducer code
export type Mutation =
| {
tag: "InitSchema";
}
| {
tag: "AddMessage";
id: string;
msg: string;
}
| {
tag: "DeleteMessage";
id: string;
};

export const TaskDocType: DocType<Mutation> = {
reducerUrl: REDUCER_URL,
serializeMutation: serializeMutationAsJSON,
};

export const { useMutate, useQuery, useSetConnectionEnabled } = createDocHooks(() => TaskDocType);
114 changes: 114 additions & 0 deletions examples/guestbook-solid-js/src/main.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { SQLSyncProvider } from "@orbitinghail/sqlsync-solid-js";
import { journalIdFromString, sql } from "@orbitinghail/sqlsync-worker";

// this example uses the uuid library (`npm install uuid`)
import { JSX } from "solid-js/jsx-runtime";
import { v4 as uuidv4 } from "uuid";

// You'll need to configure your build system to make these entrypoints
// available as urls. Vite does this automatically via the `?url` and `?worker&url` suffix.
import sqlSyncWasmUrl from "@orbitinghail/sqlsync-worker/sqlsync.wasm?url";
import workerUrl from "@orbitinghail/sqlsync-worker/worker.ts?worker&url";

import { createEffect, createSignal } from "solid-js";
import { For, render } from "solid-js/web";
import { useMutate, useQuery } from "./doctype";

// Create a DOC_ID to use, each DOC_ID will correspond to a different SQLite
// database. We use a static doc id so we can play with cross-tab sync.
const DOC_ID = journalIdFromString("VM7fC4gKxa52pbdtrgd9G9");

// Use SQLSync hooks in your app
export function App() {
// we will use the standard useState hook to handle the message input box
const [msg, setMsg] = createSignal("");

// create a mutate function for our document
const mutate = useMutate(DOC_ID);

// initialize the schema; eventually this will be handled by SQLSync automatically
createEffect(() => {
mutate({ tag: "InitSchema" }).catch((err) => {
console.error("Failed to init schema", err);
});
});

// create a callback which knows how to trigger the add message mutation
const handleSubmit: JSX.EventHandler<HTMLFormElement, SubmitEvent> = (e) => {
// Prevent the browser from reloading the page
e.preventDefault();

// create a unique message id
const id = crypto.randomUUID ? crypto.randomUUID() : uuidv4();

// don't add empty messages
if (msg().trim() !== "") {
mutate({ tag: "AddMessage", id, msg: msg() }).catch((err) => {
console.error("Failed to add message", err);
});
// clear the message
setMsg("");
}
};

// create a callback to delete a message
const handleDelete = (id: string) => {
mutate({ tag: "DeleteMessage", id }).catch((err) => {
console.error("Failed to delete message", err);
});
};

// finally, query SQLSync for all the messages, sorted by created_at
const queryState = useQuery<{ id: string; msg: string }>(
() => DOC_ID,
() => sql`
select id, msg from messages
order by created_at
`
);

const rows = () => queryState().rows ?? [];

return (
<div>
<h1>Guestbook:</h1>
<ul>
<For each={rows()}>
{(row) => {
return (
<li>
{row.msg}
<button
type="button"
onClick={() => handleDelete(row.id)}
style={{ "margin-left": "40px" }}
>
remove msg
</button>
</li>
);
}}
</For>
</ul>
<h3>Leave a message:</h3>
<form onSubmit={handleSubmit}>
<label>
Msg:
<input type="text" name="msg" value={msg()} onChange={(e) => setMsg(e.target.value)} />
</label>
<input type="submit" value="Submit" />
</form>
</div>
);
}

// Configure the SQLSync provider near the top of the React tree
// biome-ignore lint/style/noNonNullAssertion: we know this element exists
render(
() => (
<SQLSyncProvider wasmUrl={sqlSyncWasmUrl} workerUrl={workerUrl}>
<App />
</SQLSyncProvider>
),
document.getElementById("root")!
);
matthewgapp marked this conversation as resolved.
Show resolved Hide resolved
27 changes: 27 additions & 0 deletions examples/guestbook-solid-js/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "ES2021.WeakRef", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"types": ["vite/client"],

/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
"jsxImportSource": "solid-js",

/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}
10 changes: 10 additions & 0 deletions examples/guestbook-solid-js/tsconfig.node.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}
7 changes: 7 additions & 0 deletions examples/guestbook-solid-js/vite.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { defineConfig } from "vite";
import solid from "vite-plugin-solid";

// https://vitejs.dev/config/
export default defineConfig({
plugins: [solid()],
});
6 changes: 6 additions & 0 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ wasm-examples-reducer-guestbook:
example-guestbook-react: wasm-examples-reducer-guestbook
cd examples/guestbook-react && pnpm dev

example-guestbook-solid-js: wasm-examples-reducer-guestbook
cd examples/guestbook-solid-js && pnpm dev

test-end-to-end-local rng_seed="": wasm-task-reducer
RUST_BACKTRACE=1 cargo run --example end-to-end-local {{rng_seed}}

Expand All @@ -59,6 +62,9 @@ node_modules:
package-sqlsync-react:
cd lib/sqlsync-react && pnpm build

package-sqlsync-solid-js:
cd lib/sqlsync-solid-js && pnpm build

package-sqlsync-worker target='release':
#!/usr/bin/env bash
if [[ '{{target}}' = 'release' ]]; then
Expand Down
58 changes: 58 additions & 0 deletions lib/sqlsync-solid-js/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
{
"name": "@orbitinghail/sqlsync-solid-js",
"version": "0.2.0",
"description": "SQLSync is a collaborative offline-first wrapper around SQLite. It is designed to synchronize web application state between users, devices, and the edge.",
"homepage": "https://sqlsync.dev",
"license": "Apache-2.0",
"keywords": [
"sqlsync",
"sql",
"database",
"sqlite",
"offline-first",
"local-first",
"solid-js"
],
"repository": {
"type": "git",
"url": "https://github.com/orbitinghail/sqlsync"
},
"files": [
"dist",
"src"
],
"type": "module",
"main": "./dist/sqlsync-solid-js.js",
"types": "./src/index.ts",
"exports": {
".": {
"import": "./dist/sqlsync-solid-js.js",
"require": "./dist/sqlsync-solid-js.umd.cjs",
"types": "./src/index.ts"
}
},
"scripts": {
"dev": "vite",
"build": "tsc && vite build"
},
"devDependencies": {
"@types/node": "^20.8.8",
"vite-plugin-solid": "^2.7.0",
"typescript": "^5.2.2",
"vite": "^4.5.0",
"vite-plugin-dts": "^3.6.1",
"vite-tsconfig-paths": "^4.2.1",
"vitest": "^0.34.6",
"@solidjs/testing-library": "^0.8.4",
"@testing-library/jest-dom": "^6.1.3"
},
"dependencies": {
"@orbitinghail/sqlsync-worker": "0.2.0",
"@scure/base": "^1.1.3",
"fast-equals": "^5.0.1",
"fast-sha256": "^1.3.0"
},
"peerDependencies": {
"solid-js": "^1.8.7"
}
}
34 changes: 34 additions & 0 deletions lib/sqlsync-solid-js/src/context.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { SQLSync } from "@orbitinghail/sqlsync-worker";
import { ParentComponent, createContext, createSignal, onCleanup } from "solid-js";

export const SQLSyncContext = createContext<[() => SQLSync | null, (sqlSync: SQLSync) => void]>([
() => null,
() => {},
]);

interface Props {
workerUrl: string | URL;
wasmUrl: string | URL;
coordinatorUrl?: string | URL;
}

export const createSqlSync = (props: Props): SQLSync => {
return new SQLSync(props.workerUrl, props.wasmUrl, props.coordinatorUrl);
};

export const SQLSyncProvider: ParentComponent<Props> = (props) => {
const [sqlSync, setSQLSync] = createSignal<SQLSync>(createSqlSync(props));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not super familiar with how SolidJS works, but it appears that every time this component (SQLSyncProvider) renders it will re-create the SQLSync object. This is undesirable as each instance of the SQLSync object will initialize a connection to the shared worker at construction time. Is it possible to only call createSqlSync once if the signal has not yet been set?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Solid JS calls component functions once, so this should correctly create the SQLSync object once.

See here for more info: https://www.solidjs.com/guides/getting-started#2-vanishing-components


onCleanup(() => {
const s = sqlSync();
if (s) {
s.close();
}
});

return (
<SQLSyncContext.Provider value={[sqlSync, setSQLSync]}>
{props.children}
</SQLSyncContext.Provider>
);
};
Loading