From 222b80618e410493370611ecd575012f5b573a99 Mon Sep 17 00:00:00 2001
From: Matthew Gapp <61894094+matthewgapp@users.noreply.github.com>
Date: Fri, 29 Dec 2023 20:08:31 -0800
Subject: [PATCH 01/22] started adding solid js library
---
lib/sqlsync-solid-js/index.html | 16 +
lib/sqlsync-solid-js/package.json | 60 ++++
.../sqlsync-react-test-reducer/Cargo.toml | 14 +
.../sqlsync-react-test-reducer/src/lib.rs | 53 +++
lib/sqlsync-solid-js/src/context.tsx | 38 +++
lib/sqlsync-solid-js/src/hooks.ts | 119 +++++++
lib/sqlsync-solid-js/src/index.ts | 10 +
lib/sqlsync-solid-js/src/sql.ts | 42 +++
lib/sqlsync-solid-js/src/sqlsync.ts | 306 ++++++++++++++++++
lib/sqlsync-solid-js/src/util.ts | 63 ++++
lib/sqlsync-solid-js/test/react-sanity.html | 15 +
lib/sqlsync-solid-js/test/react-sanity.tsx | 96 ++++++
lib/sqlsync-solid-js/tsconfig.json | 39 +++
lib/sqlsync-solid-js/tsconfig.node.json | 12 +
lib/sqlsync-solid-js/vite.config.ts | 35 ++
pnpm-workspace.yaml | 1 +
16 files changed, 919 insertions(+)
create mode 100644 lib/sqlsync-solid-js/index.html
create mode 100644 lib/sqlsync-solid-js/package.json
create mode 100644 lib/sqlsync-solid-js/sqlsync-react-test-reducer/Cargo.toml
create mode 100644 lib/sqlsync-solid-js/sqlsync-react-test-reducer/src/lib.rs
create mode 100644 lib/sqlsync-solid-js/src/context.tsx
create mode 100644 lib/sqlsync-solid-js/src/hooks.ts
create mode 100644 lib/sqlsync-solid-js/src/index.ts
create mode 100644 lib/sqlsync-solid-js/src/sql.ts
create mode 100644 lib/sqlsync-solid-js/src/sqlsync.ts
create mode 100644 lib/sqlsync-solid-js/src/util.ts
create mode 100644 lib/sqlsync-solid-js/test/react-sanity.html
create mode 100644 lib/sqlsync-solid-js/test/react-sanity.tsx
create mode 100644 lib/sqlsync-solid-js/tsconfig.json
create mode 100644 lib/sqlsync-solid-js/tsconfig.node.json
create mode 100644 lib/sqlsync-solid-js/vite.config.ts
diff --git a/lib/sqlsync-solid-js/index.html b/lib/sqlsync-solid-js/index.html
new file mode 100644
index 0000000..4fb2a86
--- /dev/null
+++ b/lib/sqlsync-solid-js/index.html
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+ sqlsync-react tests index
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/lib/sqlsync-solid-js/package.json b/lib/sqlsync-solid-js/package.json
new file mode 100644
index 0000000..4e70f95
--- /dev/null
+++ b/lib/sqlsync-solid-js/package.json
@@ -0,0 +1,60 @@
+{
+ "name": "@orbitinghail/sqlsync-react-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-react.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"
+ },
+ "peerDependencies": {
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0"
+ },
+ "dependencies": {
+ "solid-js": "^1.8.7",
+ "@orbitinghail/sqlsync-worker": "workspace:^",
+ "@scure/base": "^1.1.3",
+ "fast-equals": "^5.0.1",
+ "fast-sha256": "^1.3.0"
+ }
+}
diff --git a/lib/sqlsync-solid-js/sqlsync-react-test-reducer/Cargo.toml b/lib/sqlsync-solid-js/sqlsync-react-test-reducer/Cargo.toml
new file mode 100644
index 0000000..fd36407
--- /dev/null
+++ b/lib/sqlsync-solid-js/sqlsync-react-test-reducer/Cargo.toml
@@ -0,0 +1,14 @@
+[package]
+name = "sqlsync-react-test-reducer"
+version = "0.1.0"
+edition = "2021"
+
+[lib]
+crate-type = ["cdylib"]
+
+[dependencies]
+sqlsync-reducer = { path = "../../sqlsync-reducer" }
+serde = { version = "1.0.171", features = ["derive"] }
+serde_json = "1.0.105"
+log = "0.4.20"
+futures = "0.3.28"
diff --git a/lib/sqlsync-solid-js/sqlsync-react-test-reducer/src/lib.rs b/lib/sqlsync-solid-js/sqlsync-react-test-reducer/src/lib.rs
new file mode 100644
index 0000000..2dbcdfd
--- /dev/null
+++ b/lib/sqlsync-solid-js/sqlsync-react-test-reducer/src/lib.rs
@@ -0,0 +1,53 @@
+// build: "cargo build --target wasm32-unknown-unknown -p counter-reducer"
+use serde::{Deserialize, Serialize};
+use sqlsync_reducer::{execute, init_reducer, types::ReducerError};
+
+#[derive(Serialize, Deserialize, Debug)]
+#[serde(tag = "tag")]
+enum Mutation {
+ InitSchema,
+ Incr { value: i32 },
+ Decr { value: i32 },
+}
+
+init_reducer!(reducer);
+async fn reducer(mutation: Vec) -> Result<(), ReducerError> {
+ let mutation: Mutation = serde_json::from_slice(&mutation[..])?;
+
+ match mutation {
+ Mutation::InitSchema => {
+ let create_table = execute!(
+ "CREATE TABLE IF NOT EXISTS counter (
+ id INTEGER PRIMARY KEY,
+ value INTEGER
+ )"
+ );
+ let init_counter = execute!(
+ "INSERT OR IGNORE INTO counter (id, value) VALUES (0, 0)"
+ );
+
+ create_table.await?;
+ init_counter.await?;
+ }
+
+ Mutation::Incr { value } => {
+ execute!(
+ "INSERT INTO counter (id, value) VALUES (0, 0)
+ ON CONFLICT (id) DO UPDATE SET value = value + ?",
+ value
+ )
+ .await?;
+ }
+
+ Mutation::Decr { value } => {
+ execute!(
+ "INSERT INTO counter (id, value) VALUES (0, 0)
+ ON CONFLICT (id) DO UPDATE SET value = value - ?",
+ value
+ )
+ .await?;
+ }
+ }
+
+ Ok(())
+}
diff --git a/lib/sqlsync-solid-js/src/context.tsx b/lib/sqlsync-solid-js/src/context.tsx
new file mode 100644
index 0000000..116e58f
--- /dev/null
+++ b/lib/sqlsync-solid-js/src/context.tsx
@@ -0,0 +1,38 @@
+// import { ReactNode, createContext, useEffect, useState } from "react";
+import {
+ ParentComponent,
+ Show,
+ createContext,
+ createEffect,
+ createSignal,
+ onCleanup,
+} from "solid-js";
+import { SQLSync } from "./sqlsync";
+
+export const SQLSyncContext = createContext(null);
+
+interface Props {
+ workerUrl: string | URL;
+ wasmUrl: string | URL;
+ coordinatorUrl?: string | URL;
+}
+
+export const SQLSyncProvider: ParentComponent = (props) => {
+ const [sqlsync, setSQLSync] = createSignal(null);
+
+ createEffect(() => {
+ const sqlsync = new SQLSync(props.workerUrl, props.wasmUrl, props.coordinatorUrl);
+ setSQLSync(sqlsync);
+ onCleanup(() => {
+ sqlsync.close();
+ });
+ });
+
+ return (
+
+ {(sqlSync) => {
+ return {props.children};
+ }}
+
+ );
+};
diff --git a/lib/sqlsync-solid-js/src/hooks.ts b/lib/sqlsync-solid-js/src/hooks.ts
new file mode 100644
index 0000000..7932e8b
--- /dev/null
+++ b/lib/sqlsync-solid-js/src/hooks.ts
@@ -0,0 +1,119 @@
+import { ConnectionStatus, DocId } from "@orbitinghail/sqlsync-worker";
+import { deepEqual } from "fast-equals";
+// import { useCallback, useContext, useEffect, useRef, useState } from "react";
+import { createContext, createMemo } from "solid-js";
+import { SQLSyncContext } from "./context";
+import { ParameterizedQuery, normalizeQuery } from "./sql";
+import { DocType, QuerySubscription, Row, SQLSync } from "./sqlsync";
+import { pendingPromise } from "./util";
+
+export function useSQLSync(): SQLSync {
+ const value = createContext(SQLSyncContext);
+ if (import.meta.env.DEV && !value) {
+ throw new Error(
+ "could not find sqlsync context value; please ensure the component is wrapped in a "
+ );
+ }
+ // biome-ignore lint/style/noNonNullAssertion: asserts in dev
+ return value!;
+}
+
+type MutateFn = (mutation: M) => Promise;
+type UseMutateFn = (docId: DocId) => MutateFn;
+
+type UseQueryFn = (docId: DocId, query: ParameterizedQuery | string) => QueryState;
+
+type SetConnectionEnabledFn = (enabled: boolean) => Promise;
+type UseSetConnectionEnabledFn = (docId: DocId) => SetConnectionEnabledFn;
+
+export interface DocHooks {
+ useMutate: UseMutateFn;
+ useQuery: UseQueryFn;
+ useSetConnectionEnabled: UseSetConnectionEnabledFn;
+}
+
+export function createDocHooks(docType: DocType): DocHooks {
+ const useMutate = (docId: DocId): MutateFn => {
+ const sqlsync = useSQLSync();
+ return createMemo((mutation: M) => sqlsync.mutate(docId, docType, mutation));
+ };
+
+ const useQueryWrapper = (docId: DocId, query: ParameterizedQuery | string) => {
+ return useQuery(docType, docId, query);
+ };
+
+ const useSetConnectionEnabledWrapper = (docId: DocId) => {
+ const sqlsync = useSQLSync();
+ return useCallback(
+ (enabled: boolean) => sqlsync.setConnectionEnabled(docId, docType, enabled),
+ [sqlsync, docId, docType]
+ );
+ };
+
+ return {
+ useMutate,
+ useQuery: useQueryWrapper,
+ useSetConnectionEnabled: useSetConnectionEnabledWrapper,
+ };
+}
+
+export type QueryState =
+ | { state: "pending"; rows?: R[] }
+ | { state: "success"; rows: R[] }
+ | { state: "error"; error: Error; rows?: R[] };
+
+export function useQuery(
+ docType: DocType,
+ docId: DocId,
+ rawQuery: ParameterizedQuery | string
+): QueryState {
+ const sqlsync = useSQLSync();
+ const [state, setState] = useState>({ state: "pending" });
+
+ // memoize query based on deep equality
+ let query = normalizeQuery(rawQuery);
+ const queryRef = useRef(query);
+ if (!deepEqual(queryRef.current, query)) {
+ queryRef.current = query;
+ }
+ query = queryRef.current;
+
+ useEffect(() => {
+ const [unsubPromise, unsubResolve] = pendingPromise<() => void>();
+
+ const subscription: QuerySubscription = {
+ handleRows: (rows: Row[]) => setState({ state: "success", rows: rows as R[] }),
+ handleErr: (err: string) =>
+ setState((s) => ({
+ state: "error",
+ error: new Error(err),
+ rows: s.rows,
+ })),
+ };
+
+ sqlsync
+ .subscribe(docId, docType, query, subscription)
+ .then(unsubResolve)
+ .catch((err: Error) => {
+ console.error("sqlsync: error subscribing", err);
+ setState({ state: "error", error: err });
+ });
+
+ return () => {
+ unsubPromise
+ .then((unsub) => unsub())
+ .catch((err) => {
+ console.error("sqlsync: error unsubscribing", err);
+ });
+ };
+ }, [sqlsync, docId, docType, query]);
+
+ return state;
+}
+
+export const useConnectionStatus = (): ConnectionStatus => {
+ const sqlsync = useSQLSync();
+ const [status, setStatus] = useState(sqlsync.connectionStatus);
+ useEffect(() => sqlsync.addConnectionStatusListener(setStatus), [sqlsync]);
+ return status;
+};
diff --git a/lib/sqlsync-solid-js/src/index.ts b/lib/sqlsync-solid-js/src/index.ts
new file mode 100644
index 0000000..44ca817
--- /dev/null
+++ b/lib/sqlsync-solid-js/src/index.ts
@@ -0,0 +1,10 @@
+import { SQLSyncProvider } from "./context";
+import { createDocHooks, useConnectionStatus } from "./hooks";
+import { sql } from "./sql";
+import { DocType, Row } from "./sqlsync";
+import { serializeMutationAsJSON } from "./util";
+
+export { SQLSyncProvider, createDocHooks, serializeMutationAsJSON, sql, useConnectionStatus };
+export type { DocType, Row };
+
+// eof: this file only exports
diff --git a/lib/sqlsync-solid-js/src/sql.ts b/lib/sqlsync-solid-js/src/sql.ts
new file mode 100644
index 0000000..195d712
--- /dev/null
+++ b/lib/sqlsync-solid-js/src/sql.ts
@@ -0,0 +1,42 @@
+import { QueryKey, SqlValue } from "@orbitinghail/sqlsync-worker";
+import { base58 } from "@scure/base";
+import { sha256Digest } from "./util";
+
+const UTF8_ENCODER = new TextEncoder();
+
+export interface ParameterizedQuery {
+ sql: string;
+ params: SqlValue[];
+}
+
+export function normalizeQuery(query: ParameterizedQuery | string): ParameterizedQuery {
+ if (typeof query === "string") {
+ return { sql: query, params: [] };
+ }
+ return query;
+}
+
+/**
+ * Returns a parameterized query object with the given SQL string and parameters.
+ * This function should be used as a template literal tag.
+ *
+ * @example
+ * const query = sql`SELECT * FROM users WHERE id = ${userId}`;
+ *
+ * @param chunks - An array of string literals.
+ * @param params - An array of parameter values to be inserted into the SQL string.
+ * @returns A parameterized query object with the given SQL string and parameters.
+ */
+export function sql(chunks: readonly string[], ...params: SqlValue[]): ParameterizedQuery {
+ return {
+ sql: chunks.join("?"),
+ params,
+ };
+}
+
+export async function toQueryKey(query: ParameterizedQuery): Promise {
+ const queryJson = JSON.stringify([query.sql, query.params]);
+ const encoded = UTF8_ENCODER.encode(queryJson);
+ const hashed = await sha256Digest(encoded);
+ return base58.encode(new Uint8Array(hashed));
+}
diff --git a/lib/sqlsync-solid-js/src/sqlsync.ts b/lib/sqlsync-solid-js/src/sqlsync.ts
new file mode 100644
index 0000000..0321c53
--- /dev/null
+++ b/lib/sqlsync-solid-js/src/sqlsync.ts
@@ -0,0 +1,306 @@
+import {
+ ConnectionStatus,
+ DocEvent,
+ DocId,
+ DocReply,
+ HandlerId,
+ QueryKey,
+ SqlValue,
+ WorkerRequest,
+ WorkerToHostMsg,
+ journalIdToString,
+} from "@orbitinghail/sqlsync-worker";
+import { ParameterizedQuery, toQueryKey } from "./sql";
+import { NarrowTaggedEnum, OmitUnion, assertUnreachable, initWorker, toRows } from "./util";
+
+export type Row = Record;
+
+export interface DocType {
+ readonly reducerUrl: string | URL;
+ readonly serializeMutation: (mutation: Mutation) => Uint8Array;
+}
+
+type DocReplyTag = DocReply["tag"];
+type SelectDocReply = NarrowTaggedEnum;
+
+export interface QuerySubscription {
+ handleRows: (rows: Row[]) => void;
+ handleErr: (err: string) => void;
+}
+
+const nextHandlerId = (() => {
+ let handlerId = 0;
+ return () => handlerId++;
+})();
+
+export class SQLSync {
+ #port: MessagePort;
+ #openDocs = new Set();
+ #pendingOpens = new Map>();
+ #msgHandlers = new Map void>();
+ #querySubscriptions = new Map();
+ #connectionStatus: ConnectionStatus = "disconnected";
+ #connectionStatusListeners = new Set<(status: ConnectionStatus) => void>();
+
+ constructor(workerUrl: string | URL, wasmUrl: string | URL, coordinatorUrl?: string | URL) {
+ this.#msgHandlers = new Map();
+ const port = initWorker(workerUrl);
+ this.#port = port;
+
+ // We use a WeakRef here to avoid a circular reference between this.port and this.
+ // This allows the SQLSync object to be garbage collected when it is no longer needed.
+ const weakThis = new WeakRef(this);
+ this.#port.onmessage = (msg) => {
+ const thisRef = weakThis.deref();
+ if (thisRef) {
+ thisRef.#handleMessage(msg);
+ } else {
+ console.log(
+ "sqlsync: dropping message; sqlsync object has been garbage collected",
+ msg.data,
+ );
+ // clean up the port
+ port.postMessage({ tag: "Close", handlerId: 0 });
+ port.onmessage = null;
+ return;
+ }
+ };
+
+ this.#boot(wasmUrl.toString(), coordinatorUrl?.toString()).catch((err) => {
+ // TODO: expose this error to the app in a nicer way
+ // probably through some event handlers on the SQLSync object
+ console.error("sqlsync boot failed", err);
+ throw err;
+ });
+ }
+
+ close() {
+ this.#port.onmessage = null;
+ this.#port.postMessage({ tag: "Close", handlerId: 0 });
+ }
+
+ #handleMessage(event: MessageEvent) {
+ const msg = event.data as WorkerToHostMsg;
+
+ if (msg.tag === "Reply") {
+ console.log("sqlsync: received reply", msg.handlerId, msg.reply);
+ const handler = this.#msgHandlers.get(msg.handlerId);
+ if (handler) {
+ handler(msg.reply);
+ } else {
+ console.error("sqlsync: no handler for message", msg);
+ throw new Error("no handler for message");
+ }
+ } else if (msg.tag === "Event") {
+ this.#handleDocEvent(msg.docId, msg.evt);
+ } else {
+ assertUnreachable("unknown message", msg);
+ }
+ }
+
+ #handleDocEvent(docId: DocId, evt: DocEvent) {
+ console.log(`sqlsync: doc ${journalIdToString(docId)} received event`, evt);
+ if (evt.tag === "ConnectionStatus") {
+ this.#connectionStatus = evt.status;
+ for (const listener of this.#connectionStatusListeners) {
+ listener(evt.status);
+ }
+ } else if (evt.tag === "SubscriptionChanged") {
+ const subscriptions = this.#querySubscriptions.get(evt.key);
+ if (subscriptions) {
+ for (const subscription of subscriptions) {
+ subscription.handleRows(toRows(evt.columns, evt.rows));
+ }
+ }
+ } else if (evt.tag === "SubscriptionErr") {
+ const subscriptions = this.#querySubscriptions.get(evt.key);
+ if (subscriptions) {
+ for (const subscription of subscriptions) {
+ subscription.handleErr(evt.err);
+ }
+ }
+ } else {
+ assertUnreachable("unknown event", evt);
+ }
+ }
+
+ #send>(
+ expectedReplyTag: T,
+ msg: OmitUnion,
+ ): Promise> {
+ return new Promise((resolve, reject) => {
+ const handlerId = nextHandlerId();
+ const req: WorkerRequest = { ...msg, handlerId };
+
+ console.log("sqlsync: sending message", req.handlerId, req.tag === "Doc" ? req.req : req);
+
+ this.#msgHandlers.set(handlerId, (msg: DocReply) => {
+ this.#msgHandlers.delete(handlerId);
+ if (msg.tag === "Err") {
+ reject(msg.err);
+ } else if (msg.tag === expectedReplyTag) {
+ // TODO: is it possible to get Typescript to infer this cast?
+ resolve(msg as SelectDocReply);
+ } else {
+ console.warn("sqlsync: unexpected reply", msg);
+ reject(new Error(`expected ${expectedReplyTag} reply; got ${msg.tag}`));
+ }
+ });
+
+ this.#port.postMessage(req);
+ });
+ }
+
+ async #boot(wasmUrl: string, coordinatorUrl?: string): Promise {
+ await this.#send("Ack", {
+ tag: "Boot",
+ wasmUrl,
+ coordinatorUrl,
+ });
+ }
+
+ async #open(docId: DocId, docType: DocType): Promise {
+ let openPromise = this.#pendingOpens.get(docId);
+ if (!openPromise) {
+ openPromise = this.#send("Ack", {
+ tag: "Doc",
+ docId,
+ req: {
+ tag: "Open",
+ reducerUrl: docType.reducerUrl.toString(),
+ },
+ });
+ this.#pendingOpens.set(docId, openPromise);
+ }
+ await openPromise;
+ this.#pendingOpens.delete(docId);
+ this.#openDocs.add(docId);
+ }
+
+ async query(
+ docId: DocId,
+ docType: DocType,
+ sql: string,
+ params: SqlValue[],
+ ): Promise {
+ if (!this.#openDocs.has(docId)) {
+ await this.#open(docId, docType);
+ }
+
+ const reply = await this.#send("RecordSet", {
+ tag: "Doc",
+ docId: docId,
+ req: { tag: "Query", sql, params },
+ });
+
+ return toRows(reply.columns, reply.rows);
+ }
+
+ async subscribe(
+ docId: DocId,
+ docType: DocType,
+ query: ParameterizedQuery,
+ subscription: QuerySubscription,
+ ): Promise<() => void> {
+ if (!this.#openDocs.has(docId)) {
+ await this.#open(docId, docType);
+ }
+ const queryKey = await toQueryKey(query);
+
+ // get or create subscription
+ let subscriptions = this.#querySubscriptions.get(queryKey);
+ if (!subscriptions) {
+ subscriptions = [];
+ this.#querySubscriptions.set(queryKey, subscriptions);
+ }
+ if (subscriptions.indexOf(subscription) === -1) {
+ subscriptions.push(subscription);
+ } else {
+ throw new Error("sqlsync: duplicate subscription");
+ }
+
+ // send subscribe request
+ await this.#send("Ack", {
+ tag: "Doc",
+ docId,
+ req: { tag: "QuerySubscribe", key: queryKey, sql: query.sql, params: query.params },
+ });
+
+ // return unsubscribe function
+ return () => {
+ const subscriptions = this.#querySubscriptions.get(queryKey);
+ if (!subscriptions) {
+ // no subscriptions
+ return;
+ }
+ const idx = subscriptions.indexOf(subscription);
+ if (idx === -1) {
+ // no subscription
+ return;
+ }
+ subscriptions.splice(idx, 1);
+
+ window.setTimeout(() => {
+ // we want to wait a tiny bit before sending finalizing the unsubscribe
+ // to handle the case that React resubscribes to the same query right away
+ this.#unsubscribeIfNeeded(docId, queryKey).catch((err) => {
+ console.error("sqlsync: error unsubscribing", err);
+ });
+ }, 10);
+ };
+ }
+
+ async #unsubscribeIfNeeded(docId: DocId, queryKey: QueryKey): Promise {
+ const subscriptions = this.#querySubscriptions.get(queryKey);
+ if (Array.isArray(subscriptions) && subscriptions.length === 0) {
+ // query subscription is still registered but has no subscriptions on our side
+ // inform the worker that we are no longer interested in this query
+ this.#querySubscriptions.delete(queryKey);
+
+ if (this.#openDocs.has(docId)) {
+ await this.#send("Ack", {
+ tag: "Doc",
+ docId,
+ req: { tag: "QueryUnsubscribe", key: queryKey },
+ });
+ }
+ }
+ }
+
+ async mutate(docId: DocId, docType: DocType, mutation: M): Promise {
+ if (!this.#openDocs.has(docId)) {
+ await this.#open(docId, docType);
+ }
+ await this.#send("Ack", {
+ tag: "Doc",
+ docId,
+ req: { tag: "Mutate", mutation: docType.serializeMutation(mutation) },
+ });
+ }
+
+ get connectionStatus(): ConnectionStatus {
+ return this.#connectionStatus;
+ }
+
+ addConnectionStatusListener(listener: (status: ConnectionStatus) => void): () => void {
+ this.#connectionStatusListeners.add(listener);
+ return () => {
+ this.#connectionStatusListeners.delete(listener);
+ };
+ }
+
+ async setConnectionEnabled(
+ docId: DocId,
+ docType: DocType,
+ enabled: boolean,
+ ): Promise {
+ if (!this.#openDocs.has(docId)) {
+ await this.#open(docId, docType);
+ }
+ await this.#send("Ack", {
+ tag: "Doc",
+ docId,
+ req: { tag: "SetConnectionEnabled", enabled },
+ });
+ }
+}
diff --git a/lib/sqlsync-solid-js/src/util.ts b/lib/sqlsync-solid-js/src/util.ts
new file mode 100644
index 0000000..902a705
--- /dev/null
+++ b/lib/sqlsync-solid-js/src/util.ts
@@ -0,0 +1,63 @@
+import { SqlValue } from "@orbitinghail/sqlsync-worker";
+import * as sha256 from "fast-sha256";
+import { Row } from "./sqlsync";
+
+// omits the given keys from each member of the union
+// https://stackoverflow.com/a/57103940/65872
+// biome-ignore lint/suspicious/noExplicitAny: any is required for this type magic to work
+export type OmitUnion = T extends any ? Omit : never;
+
+export type NarrowTaggedEnum = E extends { tag: T } ? E : never;
+
+export function assertUnreachable(err: string, x: never): never {
+ throw new Error(`unreachable: ${err}; got ${JSON.stringify(x)}`);
+}
+
+export function initWorker(workerUrl: string | URL): MessagePort {
+ const type: WorkerType = workerUrl.toString().endsWith(".cjs") ? "classic" : "module";
+
+ if (typeof SharedWorker !== "undefined") {
+ const worker = new SharedWorker(workerUrl, { type });
+ return worker.port;
+ }
+
+ const worker = new Worker(workerUrl, { type });
+ // biome-ignore lint/suspicious/noExplicitAny: WebWorker extends MessagePort via duck typing
+ return worker as any as MessagePort;
+}
+
+const UTF8Encoder = new TextEncoder();
+export const serializeMutationAsJSON = (mutation: M) => {
+ const serialized = JSON.stringify(mutation);
+ return UTF8Encoder.encode(serialized);
+};
+
+export function toRows(columns: string[], rows: SqlValue[][]): R[] {
+ const out: R[] = [];
+ for (const row of rows) {
+ const obj: Row = {};
+ for (let i = 0; i < columns.length; i++) {
+ obj[columns[i]] = row[i];
+ }
+ out.push(obj as R);
+ }
+ return out;
+}
+
+export const pendingPromise = (): [Promise, (v: T) => void] => {
+ let resolve: (v: T) => void;
+ const promise = new Promise((r) => {
+ resolve = r;
+ });
+ // biome-ignore lint/style/noNonNullAssertion: we know resolve is defined because the promise constructor runs syncronously
+ return [promise, resolve!];
+};
+
+export const sha256Digest = async (data: Uint8Array): Promise => {
+ if (crypto?.subtle?.digest) {
+ const hash = await crypto.subtle.digest("SHA-256", data);
+ return new Uint8Array(hash);
+ }
+
+ return Promise.resolve(sha256.hash(data));
+};
diff --git a/lib/sqlsync-solid-js/test/react-sanity.html b/lib/sqlsync-solid-js/test/react-sanity.html
new file mode 100644
index 0000000..b7240f2
--- /dev/null
+++ b/lib/sqlsync-solid-js/test/react-sanity.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+ sqlsync-react tests
+
+
+
+ if nothing renders, open the console
+
+
+
+
\ No newline at end of file
diff --git a/lib/sqlsync-solid-js/test/react-sanity.tsx b/lib/sqlsync-solid-js/test/react-sanity.tsx
new file mode 100644
index 0000000..83d80cf
--- /dev/null
+++ b/lib/sqlsync-solid-js/test/react-sanity.tsx
@@ -0,0 +1,96 @@
+import React, { useEffect } from "react";
+
+import { JournalId, journalIdFromString } from "@orbitinghail/sqlsync-worker";
+import { createDocHooks } from "../src/hooks";
+import { sql } from "../src/sql";
+import { DocType } from "../src/sqlsync";
+import { serializeMutationAsJSON } from "../src/util";
+
+const DEMO_REDUCER_URL = new URL(
+ "../../../target/wasm32-unknown-unknown/debug/sqlsync_react_test_reducer.wasm",
+ import.meta.url
+);
+
+const DOC_ID = journalIdFromString("VM7fC4gKxa52pbdtrgd9G9");
+
+type CounterOps =
+ | {
+ tag: "InitSchema";
+ }
+ | {
+ tag: "Incr";
+ value: number;
+ }
+ | {
+ tag: "Decr";
+ value: number;
+ };
+
+const CounterDocType: DocType = {
+ reducerUrl: DEMO_REDUCER_URL,
+ serializeMutation: serializeMutationAsJSON,
+};
+
+const { useMutate, useQuery } = createDocHooks(CounterDocType);
+
+// biome-ignore lint/style/noNonNullAssertion: root is defined
+// ReactDOM.createRoot(document.getElementById("root")!).render(
+//
+//
+//
+//
+// ,
+// );
+
+function App({ docId }: { docId: JournalId }) {
+ const mutate = useMutate(docId);
+
+ useEffect(() => {
+ mutate({ tag: "InitSchema" }).catch((err) => {
+ console.error("Failed to init schema", err);
+ });
+ }, [mutate]);
+
+ const handleIncr = React.useCallback(() => {
+ mutate({ tag: "Incr", value: 1 }).catch((err) => {
+ console.error("Failed to incr", err);
+ });
+ }, [mutate]);
+
+ const handleDecr = React.useCallback(() => {
+ mutate({ tag: "Decr", value: 1 }).catch((err) => {
+ console.error("Failed to decr", err);
+ });
+ }, [mutate]);
+
+ const query = useQuery<{ value: number }>(
+ docId,
+ sql`select value, 'hi', 1.23, ${"foo"} as s from counter`
+ );
+
+ return (
+ <>
+ sqlsync-react sanity test
+
+ This is a sanity test for sqlsync-react. It should display a counter that can be incremented
+ and decremented.
+
+ The counter is stored in a SQL database, and the state is managed by sqlsync-react.
+
+
+
+
+ {query.state === "pending" ? (
+ Loading...
+ ) : query.state === "error" ? (
+ {query.error.message}
+ ) : (
+ {query.rows[0]?.value.toString()}
+ )}
+ >
+ );
+}
diff --git a/lib/sqlsync-solid-js/tsconfig.json b/lib/sqlsync-solid-js/tsconfig.json
new file mode 100644
index 0000000..07a282b
--- /dev/null
+++ b/lib/sqlsync-solid-js/tsconfig.json
@@ -0,0 +1,39 @@
+{
+ "compilerOptions": {
+ "target": "esnext",
+ "useDefineForClassFields": true,
+ "module": "esnext",
+ "lib": [
+ "ES6",
+ "DOM",
+ "DOM.Iterable",
+ "ES2021.WeakRef"
+ ],
+ "types": [
+ "vite/client"
+ ],
+ "skipLibCheck": true,
+ "jsx": "preserve",
+ "jsxImportSource": "solid-js",
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": [
+ "src",
+ "test"
+ ],
+ "references": [
+ {
+ "path": "./tsconfig.node.json"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/lib/sqlsync-solid-js/tsconfig.node.json b/lib/sqlsync-solid-js/tsconfig.node.json
new file mode 100644
index 0000000..b6dda0b
--- /dev/null
+++ b/lib/sqlsync-solid-js/tsconfig.node.json
@@ -0,0 +1,12 @@
+{
+ "compilerOptions": {
+ "composite": true,
+ "skipLibCheck": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "allowSyntheticDefaultImports": true,
+ "types": ["node"],
+ "strict": true,
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/lib/sqlsync-solid-js/vite.config.ts b/lib/sqlsync-solid-js/vite.config.ts
new file mode 100644
index 0000000..70c1bd7
--- /dev/null
+++ b/lib/sqlsync-solid-js/vite.config.ts
@@ -0,0 +1,35 @@
+import { resolve } from "path";
+import { defineConfig, searchForWorkspaceRoot } from "vite";
+import dts from "vite-plugin-dts";
+import solidPlugin from "vite-plugin-solid";
+
+export default defineConfig({
+ plugins: [
+ solidPlugin(),
+ dts({
+ exclude: "test/**/*",
+ }),
+ ],
+ build: {
+ lib: {
+ entry: resolve(__dirname, "src/index.ts"),
+ name: "SQLSyncReact",
+ formats: ["es", "umd"],
+ },
+ sourcemap: true,
+ rollupOptions: {
+ external: ["react"],
+ output: {
+ exports: "named",
+ globals: {
+ react: "React",
+ },
+ },
+ },
+ },
+ server: {
+ fs: {
+ allow: [searchForWorkspaceRoot(process.cwd())],
+ },
+ },
+});
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index b393bd5..a0455a0 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -2,5 +2,6 @@ packages:
- "demo/cloudflare-backend"
- "demo/frontend"
- "lib/sqlsync-react"
+ - "lib/sqlsync-solid-js"
- "lib/sqlsync-worker"
- "examples/guestbook-react"
From 8eb218815bd665c36000d4285cbebae0dbc206d4 Mon Sep 17 00:00:00 2001
From: Matthew Gapp <61894094+matthewgapp@users.noreply.github.com>
Date: Sat, 30 Dec 2023 11:07:37 -0800
Subject: [PATCH 02/22] more changes. just have to fix the test
---
lib/sqlsync-solid-js/src/context.tsx | 24 +++----
lib/sqlsync-solid-js/src/hooks.ts | 82 +++++++++++++---------
lib/sqlsync-solid-js/src/sqlsync.ts | 14 ++--
lib/sqlsync-solid-js/test/react-sanity.tsx | 58 +++++++++------
4 files changed, 101 insertions(+), 77 deletions(-)
diff --git a/lib/sqlsync-solid-js/src/context.tsx b/lib/sqlsync-solid-js/src/context.tsx
index 116e58f..67f5188 100644
--- a/lib/sqlsync-solid-js/src/context.tsx
+++ b/lib/sqlsync-solid-js/src/context.tsx
@@ -1,7 +1,7 @@
// import { ReactNode, createContext, useEffect, useState } from "react";
import {
+ Accessor,
ParentComponent,
- Show,
createContext,
createEffect,
createSignal,
@@ -9,7 +9,7 @@ import {
} from "solid-js";
import { SQLSync } from "./sqlsync";
-export const SQLSyncContext = createContext(null);
+export const SQLSyncContext = createContext<[Accessor]>([() => null]);
interface Props {
workerUrl: string | URL;
@@ -17,22 +17,20 @@ interface Props {
coordinatorUrl?: string | URL;
}
+const createSqlSync = (props: Props): SQLSync => {
+ return new SQLSync(props.workerUrl, props.wasmUrl, props.coordinatorUrl);
+};
+
export const SQLSyncProvider: ParentComponent = (props) => {
- const [sqlsync, setSQLSync] = createSignal(null);
+ const [sqlSync, setSQLSync] = createSignal(null);
createEffect(() => {
- const sqlsync = new SQLSync(props.workerUrl, props.wasmUrl, props.coordinatorUrl);
- setSQLSync(sqlsync);
+ const sqlSync = createSqlSync(props);
+ setSQLSync(sqlSync);
onCleanup(() => {
- sqlsync.close();
+ sqlSync.close();
});
});
- return (
-
- {(sqlSync) => {
- return {props.children};
- }}
-
- );
+ return {props.children};
};
diff --git a/lib/sqlsync-solid-js/src/hooks.ts b/lib/sqlsync-solid-js/src/hooks.ts
index 7932e8b..fe6e7ac 100644
--- a/lib/sqlsync-solid-js/src/hooks.ts
+++ b/lib/sqlsync-solid-js/src/hooks.ts
@@ -1,27 +1,42 @@
import { ConnectionStatus, DocId } from "@orbitinghail/sqlsync-worker";
-import { deepEqual } from "fast-equals";
// import { useCallback, useContext, useEffect, useRef, useState } from "react";
-import { createContext, createMemo } from "solid-js";
+import { Accessor, createEffect, createSignal, onCleanup, useContext } from "solid-js";
import { SQLSyncContext } from "./context";
import { ParameterizedQuery, normalizeQuery } from "./sql";
import { DocType, QuerySubscription, Row, SQLSync } from "./sqlsync";
import { pendingPromise } from "./util";
-export function useSQLSync(): SQLSync {
- const value = createContext(SQLSyncContext);
- if (import.meta.env.DEV && !value) {
+export function useSQLSync(): Accessor {
+ const [value] = useContext(SQLSyncContext);
+ if (import.meta.env.DEV && !value()) {
throw new Error(
"could not find sqlsync context value; please ensure the component is wrapped in a "
);
}
+
// biome-ignore lint/style/noNonNullAssertion: asserts in dev
- return value!;
+ return () => {
+ const sqlsync = value();
+ if (import.meta.env.DEV && !sqlsync) {
+ throw new Error(
+ "could not find sqlsync context value; please ensure the component is wrapped in a "
+ );
+ } else if (!sqlsync) {
+ console.error(
+ "could not find sqlsync context value; please ensure the component is wrapped in a "
+ );
+ }
+ return sqlsync!;
+ };
}
type MutateFn = (mutation: M) => Promise;
type UseMutateFn = (docId: DocId) => MutateFn;
-type UseQueryFn = (docId: DocId, query: ParameterizedQuery | string) => QueryState;
+type UseQueryFn = (
+ docId: Accessor,
+ query: Accessor
+) => Accessor>;
type SetConnectionEnabledFn = (enabled: boolean) => Promise;
type UseSetConnectionEnabledFn = (docId: DocId) => SetConnectionEnabledFn;
@@ -32,22 +47,22 @@ export interface DocHooks {
useSetConnectionEnabled: UseSetConnectionEnabledFn;
}
-export function createDocHooks(docType: DocType): DocHooks {
+export function createDocHooks(docType: Accessor>): DocHooks {
const useMutate = (docId: DocId): MutateFn => {
const sqlsync = useSQLSync();
- return createMemo((mutation: M) => sqlsync.mutate(docId, docType, mutation));
+ return (mutation: M) => sqlsync().mutate(docId, docType(), mutation);
};
- const useQueryWrapper = (docId: DocId, query: ParameterizedQuery | string) => {
+ const useQueryWrapper = (
+ docId: Accessor,
+ query: Accessor
+ ) => {
return useQuery(docType, docId, query);
};
const useSetConnectionEnabledWrapper = (docId: DocId) => {
const sqlsync = useSQLSync();
- return useCallback(
- (enabled: boolean) => sqlsync.setConnectionEnabled(docId, docType, enabled),
- [sqlsync, docId, docType]
- );
+ return (enabled: boolean) => sqlsync().setConnectionEnabled(docId, docType(), enabled);
};
return {
@@ -63,22 +78,16 @@ export type QueryState =
| { state: "error"; error: Error; rows?: R[] };
export function useQuery(
- docType: DocType,
- docId: DocId,
- rawQuery: ParameterizedQuery | string
-): QueryState {
+ docType: Accessor>,
+ docId: Accessor,
+ rawQuery: Accessor
+): Accessor> {
const sqlsync = useSQLSync();
- const [state, setState] = useState>({ state: "pending" });
+ const [state, setState] = createSignal>({ state: "pending" });
- // memoize query based on deep equality
- let query = normalizeQuery(rawQuery);
- const queryRef = useRef(query);
- if (!deepEqual(queryRef.current, query)) {
- queryRef.current = query;
- }
- query = queryRef.current;
+ createEffect(() => {
+ let query = normalizeQuery(rawQuery());
- useEffect(() => {
const [unsubPromise, unsubResolve] = pendingPromise<() => void>();
const subscription: QuerySubscription = {
@@ -91,29 +100,32 @@ export function useQuery(
})),
};
- sqlsync
- .subscribe(docId, docType, query, subscription)
+ sqlsync()
+ .subscribe(docId(), docType(), query, subscription)
.then(unsubResolve)
.catch((err: Error) => {
console.error("sqlsync: error subscribing", err);
setState({ state: "error", error: err });
});
- return () => {
+ onCleanup(() => {
unsubPromise
.then((unsub) => unsub())
.catch((err) => {
console.error("sqlsync: error unsubscribing", err);
});
- };
- }, [sqlsync, docId, docType, query]);
+ });
+ });
return state;
}
-export const useConnectionStatus = (): ConnectionStatus => {
+export const useConnectionStatus = (): Accessor => {
const sqlsync = useSQLSync();
- const [status, setStatus] = useState(sqlsync.connectionStatus);
- useEffect(() => sqlsync.addConnectionStatusListener(setStatus), [sqlsync]);
+ const [status, setStatus] = createSignal(sqlsync().connectionStatus);
+ createEffect(() => {
+ const cleanup = sqlsync().addConnectionStatusListener(setStatus);
+ onCleanup(cleanup);
+ });
return status;
};
diff --git a/lib/sqlsync-solid-js/src/sqlsync.ts b/lib/sqlsync-solid-js/src/sqlsync.ts
index 0321c53..85ecbc0 100644
--- a/lib/sqlsync-solid-js/src/sqlsync.ts
+++ b/lib/sqlsync-solid-js/src/sqlsync.ts
@@ -57,7 +57,7 @@ export class SQLSync {
} else {
console.log(
"sqlsync: dropping message; sqlsync object has been garbage collected",
- msg.data,
+ msg.data
);
// clean up the port
port.postMessage({ tag: "Close", handlerId: 0 });
@@ -94,7 +94,7 @@ export class SQLSync {
} else if (msg.tag === "Event") {
this.#handleDocEvent(msg.docId, msg.evt);
} else {
- assertUnreachable("unknown message", msg);
+ assertUnreachable("unknown message", msg as never);
}
}
@@ -120,13 +120,13 @@ export class SQLSync {
}
}
} else {
- assertUnreachable("unknown event", evt);
+ assertUnreachable("unknown event", evt as never);
}
}
#send>(
expectedReplyTag: T,
- msg: OmitUnion,
+ msg: OmitUnion
): Promise> {
return new Promise((resolve, reject) => {
const handlerId = nextHandlerId();
@@ -181,7 +181,7 @@ export class SQLSync {
docId: DocId,
docType: DocType,
sql: string,
- params: SqlValue[],
+ params: SqlValue[]
): Promise {
if (!this.#openDocs.has(docId)) {
await this.#open(docId, docType);
@@ -200,7 +200,7 @@ export class SQLSync {
docId: DocId,
docType: DocType,
query: ParameterizedQuery,
- subscription: QuerySubscription,
+ subscription: QuerySubscription
): Promise<() => void> {
if (!this.#openDocs.has(docId)) {
await this.#open(docId, docType);
@@ -292,7 +292,7 @@ export class SQLSync {
async setConnectionEnabled(
docId: DocId,
docType: DocType,
- enabled: boolean,
+ enabled: boolean
): Promise {
if (!this.#openDocs.has(docId)) {
await this.#open(docId, docType);
diff --git a/lib/sqlsync-solid-js/test/react-sanity.tsx b/lib/sqlsync-solid-js/test/react-sanity.tsx
index 83d80cf..84809f3 100644
--- a/lib/sqlsync-solid-js/test/react-sanity.tsx
+++ b/lib/sqlsync-solid-js/test/react-sanity.tsx
@@ -1,6 +1,9 @@
-import React, { useEffect } from "react";
+// import React, { useEffect } from "react";
import { JournalId, journalIdFromString } from "@orbitinghail/sqlsync-worker";
+import { Match, Switch, createEffect } from "solid-js";
+import { createSignal } from "solid-js/types/server/reactive.js";
+import { SQLSyncProvider } from "../src";
import { createDocHooks } from "../src/hooks";
import { sql } from "../src/sql";
import { DocType } from "../src/sqlsync";
@@ -31,42 +34,43 @@ const CounterDocType: DocType = {
serializeMutation: serializeMutationAsJSON,
};
-const { useMutate, useQuery } = createDocHooks(CounterDocType);
+const [counterDocType, _setCounterDocType] = createSignal(CounterDocType);
+
+const { useMutate, useQuery } = createDocHooks(counterDocType);
// biome-ignore lint/style/noNonNullAssertion: root is defined
-// ReactDOM.createRoot(document.getElementById("root")!).render(
-//
-//
-//
-//
-// ,
-// );
+ReactDOM.createRoot(document.getElementById("root")!).render(
+
+
+
+
+
+);
function App({ docId }: { docId: JournalId }) {
const mutate = useMutate(docId);
- useEffect(() => {
+ createEffect(() => {
mutate({ tag: "InitSchema" }).catch((err) => {
console.error("Failed to init schema", err);
});
- }, [mutate]);
+ });
- const handleIncr = React.useCallback(() => {
+ const handleIncr = () => {
mutate({ tag: "Incr", value: 1 }).catch((err) => {
console.error("Failed to incr", err);
});
- }, [mutate]);
+ };
- const handleDecr = React.useCallback(() => {
+ const handleDecr = () => {
mutate({ tag: "Decr", value: 1 }).catch((err) => {
console.error("Failed to decr", err);
});
- }, [mutate]);
+ };
- const query = useQuery<{ value: number }>(
- docId,
- sql`select value, 'hi', 1.23, ${"foo"} as s from counter`
- );
+
+ const query = useQuery<{ value: number }>(() => docId, () =>
+ sql`select value, 'hi', 1.23, ${"foo"} as s from counter`);
return (
<>
@@ -84,12 +88,22 @@ function App({ docId }: { docId: JournalId }) {
Decr
- {query.state === "pending" ? (
+
+
+
+ Loading...
+
+
+
Loading...
+
+
+
+ {query().state === "pending" ? (
) : query.state === "error" ? (
- {query.error.message}
+ {query().error.message}
) : (
- {query.rows[0]?.value.toString()}
+ {query().rows?.[0]?.value.toString()}
)}
>
);
From 41cbad6adb5a6ecabc630925640e14a2a527212b Mon Sep 17 00:00:00 2001
From: Matthew Gapp <61894094+matthewgapp@users.noreply.github.com>
Date: Sun, 31 Dec 2023 10:17:08 -0800
Subject: [PATCH 03/22] wip: trying to get the library to work for host
---
justfile | 3 ++
lib/sqlsync-solid-js/package.json | 14 +++---
lib/sqlsync-solid-js/src/context.tsx | 40 ++++++++--------
lib/sqlsync-solid-js/src/context_utils.ts | 4 ++
lib/sqlsync-solid-js/src/hooks.ts | 10 +++-
lib/sqlsync-solid-js/src/index.ts | 22 +++++++--
lib/sqlsync-solid-js/test/react-sanity.tsx | 53 ++++++++++------------
7 files changed, 83 insertions(+), 63 deletions(-)
create mode 100644 lib/sqlsync-solid-js/src/context_utils.ts
diff --git a/justfile b/justfile
index 83a3164..670a197 100644
--- a/justfile
+++ b/justfile
@@ -59,6 +59,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
diff --git a/lib/sqlsync-solid-js/package.json b/lib/sqlsync-solid-js/package.json
index 4e70f95..95b2c2c 100644
--- a/lib/sqlsync-solid-js/package.json
+++ b/lib/sqlsync-solid-js/package.json
@@ -1,5 +1,5 @@
{
- "name": "@orbitinghail/sqlsync-react-solid-js",
+ "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",
@@ -22,7 +22,7 @@
"src"
],
"type": "module",
- "main": "./dist/sqlsync-react.js",
+ "main": "./dist/sqlsync-solid-js.js",
"types": "./src/index.ts",
"exports": {
".": {
@@ -46,15 +46,13 @@
"@solidjs/testing-library": "^0.8.4",
"@testing-library/jest-dom": "^6.1.3"
},
- "peerDependencies": {
- "react": "^18.2.0",
- "react-dom": "^18.2.0"
- },
"dependencies": {
- "solid-js": "^1.8.7",
- "@orbitinghail/sqlsync-worker": "workspace:^",
+ "@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"
}
}
diff --git a/lib/sqlsync-solid-js/src/context.tsx b/lib/sqlsync-solid-js/src/context.tsx
index 67f5188..22744fd 100644
--- a/lib/sqlsync-solid-js/src/context.tsx
+++ b/lib/sqlsync-solid-js/src/context.tsx
@@ -1,36 +1,36 @@
// import { ReactNode, createContext, useEffect, useState } from "react";
-import {
- Accessor,
- ParentComponent,
- createContext,
- createEffect,
- createSignal,
- onCleanup,
-} from "solid-js";
+import { ParentComponent, createSignal } from "solid-js";
+import { SQLSyncContext } from ".";
import { SQLSync } from "./sqlsync";
-export const SQLSyncContext = createContext<[Accessor]>([() => null]);
-
interface Props {
workerUrl: string | URL;
wasmUrl: string | URL;
coordinatorUrl?: string | URL;
}
-const createSqlSync = (props: Props): SQLSync => {
+export const createSqlSync = (props: Props): SQLSync => {
return new SQLSync(props.workerUrl, props.wasmUrl, props.coordinatorUrl);
};
export const SQLSyncProvider: ParentComponent = (props) => {
- const [sqlSync, setSQLSync] = createSignal(null);
+ const [sqlSync, setSQLSync] = createSignal(createSqlSync(props));
+ // console.log("sqlSync in provider:", sqlSync(), JSON.stringify(sqlSync(), null, 2));
+
+ // const sqlSyncValue: [Accessor] = [sqlSync];
- createEffect(() => {
- const sqlSync = createSqlSync(props);
- setSQLSync(sqlSync);
- onCleanup(() => {
- sqlSync.close();
- });
- });
+ // createEffect(() => {
+ // const sqlSync = createSqlSync(props);
+ // console.log("sqlSync in effect:", sqlSync, JSON.stringify(sqlSync, null, 2));
+ // setSQLSync(sqlSync);
+ // onCleanup(() => {
+ // sqlSync.close();
+ // });
+ // });
- return {props.children};
+ return (
+
+ {props.children}
+
+ );
};
diff --git a/lib/sqlsync-solid-js/src/context_utils.ts b/lib/sqlsync-solid-js/src/context_utils.ts
new file mode 100644
index 0000000..4d30364
--- /dev/null
+++ b/lib/sqlsync-solid-js/src/context_utils.ts
@@ -0,0 +1,4 @@
+import { createContext } from "solid-js";
+import { SQLSync } from "./sqlsync";
+
+export const SQLSyncContext = createContext<[() => SQLSync, (sqlSync: SQLSync) => void]>();
diff --git a/lib/sqlsync-solid-js/src/hooks.ts b/lib/sqlsync-solid-js/src/hooks.ts
index fe6e7ac..e8b1924 100644
--- a/lib/sqlsync-solid-js/src/hooks.ts
+++ b/lib/sqlsync-solid-js/src/hooks.ts
@@ -1,13 +1,19 @@
import { ConnectionStatus, DocId } from "@orbitinghail/sqlsync-worker";
// import { useCallback, useContext, useEffect, useRef, useState } from "react";
import { Accessor, createEffect, createSignal, onCleanup, useContext } from "solid-js";
-import { SQLSyncContext } from "./context";
+import { SQLSyncContext } from "./context_utils";
import { ParameterizedQuery, normalizeQuery } from "./sql";
import { DocType, QuerySubscription, Row, SQLSync } from "./sqlsync";
import { pendingPromise } from "./util";
+export const useSqlContext = () => {
+ return [useContext(SQLSyncContext)!, SQLSyncContext, useContext];
+};
+
export function useSQLSync(): Accessor {
- const [value] = useContext(SQLSyncContext);
+ console.log("context", SQLSyncContext.id.toString());
+ const [value] = useContext(SQLSyncContext)!;
+ console.log("sqlsync: useSQLSync", value, JSON.stringify(value));
if (import.meta.env.DEV && !value()) {
throw new Error(
"could not find sqlsync context value; please ensure the component is wrapped in a "
diff --git a/lib/sqlsync-solid-js/src/index.ts b/lib/sqlsync-solid-js/src/index.ts
index 44ca817..5ce778b 100644
--- a/lib/sqlsync-solid-js/src/index.ts
+++ b/lib/sqlsync-solid-js/src/index.ts
@@ -1,10 +1,24 @@
-import { SQLSyncProvider } from "./context";
-import { createDocHooks, useConnectionStatus } from "./hooks";
+import { createSignal } from "solid-js";
+import { SQLSyncProvider, createSqlSync } from "./context";
+import { SQLSyncContext } from "./context_utils";
+import { createDocHooks, useConnectionStatus, useSQLSync, useSqlContext } from "./hooks";
import { sql } from "./sql";
-import { DocType, Row } from "./sqlsync";
+import { DocType, Row, SQLSync } from "./sqlsync";
import { serializeMutationAsJSON } from "./util";
-export { SQLSyncProvider, createDocHooks, serializeMutationAsJSON, sql, useConnectionStatus };
+export {
+ SQLSync,
+ SQLSyncContext,
+ SQLSyncProvider,
+ createDocHooks,
+ createSignal,
+ createSqlSync,
+ serializeMutationAsJSON,
+ sql,
+ useConnectionStatus,
+ useSQLSync,
+ useSqlContext,
+};
export type { DocType, Row };
// eof: this file only exports
diff --git a/lib/sqlsync-solid-js/test/react-sanity.tsx b/lib/sqlsync-solid-js/test/react-sanity.tsx
index 84809f3..c4ea0a1 100644
--- a/lib/sqlsync-solid-js/test/react-sanity.tsx
+++ b/lib/sqlsync-solid-js/test/react-sanity.tsx
@@ -1,9 +1,8 @@
// import React, { useEffect } from "react";
-import { JournalId, journalIdFromString } from "@orbitinghail/sqlsync-worker";
+import { JournalId } from "@orbitinghail/sqlsync-worker";
import { Match, Switch, createEffect } from "solid-js";
import { createSignal } from "solid-js/types/server/reactive.js";
-import { SQLSyncProvider } from "../src";
import { createDocHooks } from "../src/hooks";
import { sql } from "../src/sql";
import { DocType } from "../src/sqlsync";
@@ -14,7 +13,7 @@ const DEMO_REDUCER_URL = new URL(
import.meta.url
);
-const DOC_ID = journalIdFromString("VM7fC4gKxa52pbdtrgd9G9");
+// const DOC_ID = journalIdFromString("VM7fC4gKxa52pbdtrgd9G9");
type CounterOps =
| {
@@ -39,14 +38,15 @@ const [counterDocType, _setCounterDocType] = createSignal(CounterDocType);
const { useMutate, useQuery } = createDocHooks(counterDocType);
// biome-ignore lint/style/noNonNullAssertion: root is defined
-ReactDOM.createRoot(document.getElementById("root")!).render(
-
-
-
-
-
-);
-
+// ReactDOM.createRoot(document.getElementById("root")!).render(
+//
+//
+//
+//
+//
+// );
+
+// @ts-ignore
function App({ docId }: { docId: JournalId }) {
const mutate = useMutate(docId);
@@ -68,9 +68,10 @@ function App({ docId }: { docId: JournalId }) {
});
};
-
- const query = useQuery<{ value: number }>(() => docId, () =>
- sql`select value, 'hi', 1.23, ${"foo"} as s from counter`);
+ const query = useQuery<{ value: number }>(
+ () => docId,
+ () => sql`select value, 'hi', 1.23, ${"foo"} as s from counter`
+ );
return (
<>
@@ -88,23 +89,17 @@ function App({ docId }: { docId: JournalId }) {
Decr
-
+
-
- Loading...
-
-
-
- Loading...
-
-
+ Loading...
+
+
+ {(query() as any).error.message}
+
+
+ {query().rows?.[0]?.value.toString()}
+
- {query().state === "pending" ? (
- ) : query.state === "error" ? (
- {query().error.message}
- ) : (
- {query().rows?.[0]?.value.toString()}
- )}
>
);
}
From a469fe1ff01cf9e848b9c84145ba5f38d2124ebd Mon Sep 17 00:00:00 2001
From: Matthew Gapp <61894094+matthewgapp@users.noreply.github.com>
Date: Mon, 1 Jan 2024 14:14:55 -0800
Subject: [PATCH 04/22] downstream application is consuming solid library
properly although no queries have been tested
---
.vscode/settings.json | 14 ++++++++++++++
.../Cargo.toml | 0
.../src/lib.rs | 0
lib/sqlsync-solid-js/src/context.tsx | 5 +++--
lib/sqlsync-solid-js/src/context_utils.ts | 3 ---
lib/sqlsync-solid-js/src/hooks.ts | 2 +-
lib/sqlsync-solid-js/src/index.ts | 3 +--
lib/sqlsync-solid-js/vite.config.ts | 2 +-
8 files changed, 20 insertions(+), 9 deletions(-)
rename lib/sqlsync-solid-js/{sqlsync-react-test-reducer => sqlsync-solid-js-test-reducer}/Cargo.toml (100%)
rename lib/sqlsync-solid-js/{sqlsync-react-test-reducer => sqlsync-solid-js-test-reducer}/src/lib.rs (100%)
diff --git a/.vscode/settings.json b/.vscode/settings.json
index ff9e6b7..a06f639 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -22,5 +22,19 @@
"editor.defaultFormatter": "biomejs.biome",
"editor.tabSize": 2,
"editor.insertSpaces": true
+ },
+ "workbench.colorCustomizations": {
+ "[Monokai Pro]": {
+ "editorInlayHint.background": "#f8f8f200",
+ "editorInlayHint.foreground": "#f8f8f280"
+ },
+ "editorCursor.foreground": "#fea726",
+ "editorCursor.background": "#000000",
+ "editor.wordHighlightBackground": "#3fea9a11",
+ "editor.selectionBackground": "#25a76a55",
+ "editor.findMatchBackground": "#34c78279",
+ "activityBar.background": "#2E2C35",
+ "titleBar.activeBackground": "#413E4B",
+ "titleBar.activeForeground": "#FAFAFB"
}
}
diff --git a/lib/sqlsync-solid-js/sqlsync-react-test-reducer/Cargo.toml b/lib/sqlsync-solid-js/sqlsync-solid-js-test-reducer/Cargo.toml
similarity index 100%
rename from lib/sqlsync-solid-js/sqlsync-react-test-reducer/Cargo.toml
rename to lib/sqlsync-solid-js/sqlsync-solid-js-test-reducer/Cargo.toml
diff --git a/lib/sqlsync-solid-js/sqlsync-react-test-reducer/src/lib.rs b/lib/sqlsync-solid-js/sqlsync-solid-js-test-reducer/src/lib.rs
similarity index 100%
rename from lib/sqlsync-solid-js/sqlsync-react-test-reducer/src/lib.rs
rename to lib/sqlsync-solid-js/sqlsync-solid-js-test-reducer/src/lib.rs
diff --git a/lib/sqlsync-solid-js/src/context.tsx b/lib/sqlsync-solid-js/src/context.tsx
index 22744fd..e00dffa 100644
--- a/lib/sqlsync-solid-js/src/context.tsx
+++ b/lib/sqlsync-solid-js/src/context.tsx
@@ -1,8 +1,9 @@
// import { ReactNode, createContext, useEffect, useState } from "react";
-import { ParentComponent, createSignal } from "solid-js";
-import { SQLSyncContext } from ".";
+import { ParentComponent, createContext, createSignal } from "solid-js";
import { SQLSync } from "./sqlsync";
+export const SQLSyncContext = createContext<[() => SQLSync, (sqlSync: SQLSync) => void]>();
+
interface Props {
workerUrl: string | URL;
wasmUrl: string | URL;
diff --git a/lib/sqlsync-solid-js/src/context_utils.ts b/lib/sqlsync-solid-js/src/context_utils.ts
index 4d30364..8b13789 100644
--- a/lib/sqlsync-solid-js/src/context_utils.ts
+++ b/lib/sqlsync-solid-js/src/context_utils.ts
@@ -1,4 +1 @@
-import { createContext } from "solid-js";
-import { SQLSync } from "./sqlsync";
-export const SQLSyncContext = createContext<[() => SQLSync, (sqlSync: SQLSync) => void]>();
diff --git a/lib/sqlsync-solid-js/src/hooks.ts b/lib/sqlsync-solid-js/src/hooks.ts
index e8b1924..306e7db 100644
--- a/lib/sqlsync-solid-js/src/hooks.ts
+++ b/lib/sqlsync-solid-js/src/hooks.ts
@@ -1,7 +1,7 @@
import { ConnectionStatus, DocId } from "@orbitinghail/sqlsync-worker";
// import { useCallback, useContext, useEffect, useRef, useState } from "react";
import { Accessor, createEffect, createSignal, onCleanup, useContext } from "solid-js";
-import { SQLSyncContext } from "./context_utils";
+import { SQLSyncContext } from "./context";
import { ParameterizedQuery, normalizeQuery } from "./sql";
import { DocType, QuerySubscription, Row, SQLSync } from "./sqlsync";
import { pendingPromise } from "./util";
diff --git a/lib/sqlsync-solid-js/src/index.ts b/lib/sqlsync-solid-js/src/index.ts
index 5ce778b..f0045b9 100644
--- a/lib/sqlsync-solid-js/src/index.ts
+++ b/lib/sqlsync-solid-js/src/index.ts
@@ -1,6 +1,5 @@
import { createSignal } from "solid-js";
-import { SQLSyncProvider, createSqlSync } from "./context";
-import { SQLSyncContext } from "./context_utils";
+import { SQLSyncContext, SQLSyncProvider, createSqlSync } from "./context";
import { createDocHooks, useConnectionStatus, useSQLSync, useSqlContext } from "./hooks";
import { sql } from "./sql";
import { DocType, Row, SQLSync } from "./sqlsync";
diff --git a/lib/sqlsync-solid-js/vite.config.ts b/lib/sqlsync-solid-js/vite.config.ts
index 70c1bd7..9b393bd 100644
--- a/lib/sqlsync-solid-js/vite.config.ts
+++ b/lib/sqlsync-solid-js/vite.config.ts
@@ -18,7 +18,7 @@ export default defineConfig({
},
sourcemap: true,
rollupOptions: {
- external: ["react"],
+ external: ["solid-js"],
output: {
exports: "named",
globals: {
From 08bd7457aab84263505b7be72dbe61cc0e1a8143 Mon Sep 17 00:00:00 2001
From: Matthew Gapp <61894094+matthewgapp@users.noreply.github.com>
Date: Mon, 1 Jan 2024 16:26:56 -0800
Subject: [PATCH 05/22] some cleanup
---
lib/sqlsync-solid-js/src/context.tsx | 25 ++++++++++++-------------
lib/sqlsync-solid-js/src/hooks.ts | 21 +++++++--------------
lib/sqlsync-solid-js/src/index.ts | 21 ++++-----------------
3 files changed, 23 insertions(+), 44 deletions(-)
diff --git a/lib/sqlsync-solid-js/src/context.tsx b/lib/sqlsync-solid-js/src/context.tsx
index e00dffa..1c7aa6b 100644
--- a/lib/sqlsync-solid-js/src/context.tsx
+++ b/lib/sqlsync-solid-js/src/context.tsx
@@ -1,8 +1,11 @@
// import { ReactNode, createContext, useEffect, useState } from "react";
-import { ParentComponent, createContext, createSignal } from "solid-js";
+import { ParentComponent, createContext, createEffect, createSignal, onCleanup } from "solid-js";
import { SQLSync } from "./sqlsync";
-export const SQLSyncContext = createContext<[() => SQLSync, (sqlSync: SQLSync) => void]>();
+export const SQLSyncContext = createContext<[() => SQLSync | null, (sqlSync: SQLSync) => void]>([
+ () => null,
+ () => {},
+]);
interface Props {
workerUrl: string | URL;
@@ -16,18 +19,14 @@ export const createSqlSync = (props: Props): SQLSync => {
export const SQLSyncProvider: ParentComponent = (props) => {
const [sqlSync, setSQLSync] = createSignal(createSqlSync(props));
- // console.log("sqlSync in provider:", sqlSync(), JSON.stringify(sqlSync(), null, 2));
- // const sqlSyncValue: [Accessor] = [sqlSync];
-
- // createEffect(() => {
- // const sqlSync = createSqlSync(props);
- // console.log("sqlSync in effect:", sqlSync, JSON.stringify(sqlSync, null, 2));
- // setSQLSync(sqlSync);
- // onCleanup(() => {
- // sqlSync.close();
- // });
- // });
+ createEffect(() => {
+ const sqlSync = createSqlSync(props);
+ setSQLSync(sqlSync);
+ onCleanup(() => {
+ sqlSync.close();
+ });
+ });
return (
diff --git a/lib/sqlsync-solid-js/src/hooks.ts b/lib/sqlsync-solid-js/src/hooks.ts
index 306e7db..1c86e2a 100644
--- a/lib/sqlsync-solid-js/src/hooks.ts
+++ b/lib/sqlsync-solid-js/src/hooks.ts
@@ -1,38 +1,31 @@
import { ConnectionStatus, DocId } from "@orbitinghail/sqlsync-worker";
-// import { useCallback, useContext, useEffect, useRef, useState } from "react";
import { Accessor, createEffect, createSignal, onCleanup, useContext } from "solid-js";
import { SQLSyncContext } from "./context";
import { ParameterizedQuery, normalizeQuery } from "./sql";
import { DocType, QuerySubscription, Row, SQLSync } from "./sqlsync";
import { pendingPromise } from "./util";
-export const useSqlContext = () => {
- return [useContext(SQLSyncContext)!, SQLSyncContext, useContext];
-};
-
export function useSQLSync(): Accessor {
- console.log("context", SQLSyncContext.id.toString());
- const [value] = useContext(SQLSyncContext)!;
- console.log("sqlsync: useSQLSync", value, JSON.stringify(value));
- if (import.meta.env.DEV && !value()) {
+ const [sqlSync] = useContext(SQLSyncContext);
+ if (import.meta.env.DEV && !sqlSync()) {
throw new Error(
"could not find sqlsync context value; please ensure the component is wrapped in a "
);
}
- // biome-ignore lint/style/noNonNullAssertion: asserts in dev
return () => {
- const sqlsync = value();
- if (import.meta.env.DEV && !sqlsync) {
+ const sqlSyncValue = sqlSync();
+ if (import.meta.env.DEV && !sqlSyncValue) {
throw new Error(
"could not find sqlsync context value; please ensure the component is wrapped in a "
);
- } else if (!sqlsync) {
+ } else if (!sqlSyncValue) {
console.error(
"could not find sqlsync context value; please ensure the component is wrapped in a "
);
}
- return sqlsync!;
+ // biome-ignore lint/style/noNonNullAssertion: asserts in dev
+ return sqlSyncValue!;
};
}
diff --git a/lib/sqlsync-solid-js/src/index.ts b/lib/sqlsync-solid-js/src/index.ts
index f0045b9..44ca817 100644
--- a/lib/sqlsync-solid-js/src/index.ts
+++ b/lib/sqlsync-solid-js/src/index.ts
@@ -1,23 +1,10 @@
-import { createSignal } from "solid-js";
-import { SQLSyncContext, SQLSyncProvider, createSqlSync } from "./context";
-import { createDocHooks, useConnectionStatus, useSQLSync, useSqlContext } from "./hooks";
+import { SQLSyncProvider } from "./context";
+import { createDocHooks, useConnectionStatus } from "./hooks";
import { sql } from "./sql";
-import { DocType, Row, SQLSync } from "./sqlsync";
+import { DocType, Row } from "./sqlsync";
import { serializeMutationAsJSON } from "./util";
-export {
- SQLSync,
- SQLSyncContext,
- SQLSyncProvider,
- createDocHooks,
- createSignal,
- createSqlSync,
- serializeMutationAsJSON,
- sql,
- useConnectionStatus,
- useSQLSync,
- useSqlContext,
-};
+export { SQLSyncProvider, createDocHooks, serializeMutationAsJSON, sql, useConnectionStatus };
export type { DocType, Row };
// eof: this file only exports
From 22c28c017f7164946d0d3fb221b9f3fd660d5f1d Mon Sep 17 00:00:00 2001
From: Matthew Gapp <61894094+matthewgapp@users.noreply.github.com>
Date: Mon, 1 Jan 2024 16:27:21 -0800
Subject: [PATCH 06/22] wip: more cleanup
---
lib/sqlsync-solid-js/src/context_utils.ts | 1 -
1 file changed, 1 deletion(-)
delete mode 100644 lib/sqlsync-solid-js/src/context_utils.ts
diff --git a/lib/sqlsync-solid-js/src/context_utils.ts b/lib/sqlsync-solid-js/src/context_utils.ts
deleted file mode 100644
index 8b13789..0000000
--- a/lib/sqlsync-solid-js/src/context_utils.ts
+++ /dev/null
@@ -1 +0,0 @@
-
From f97af124ce91d13698ab080ad0029ecc7217c599 Mon Sep 17 00:00:00 2001
From: Matthew Gapp <61894094+matthewgapp@users.noreply.github.com>
Date: Mon, 1 Jan 2024 16:36:57 -0800
Subject: [PATCH 07/22] wip: cleanup tests
---
.../{react-sanity.html => solid-sanity.html} | 19 ++++++--------
.../{react-sanity.tsx => solid-sanity.tsx} | 26 ++++++++++---------
2 files changed, 22 insertions(+), 23 deletions(-)
rename lib/sqlsync-solid-js/test/{react-sanity.html => solid-sanity.html} (52%)
rename lib/sqlsync-solid-js/test/{react-sanity.tsx => solid-sanity.tsx} (81%)
diff --git a/lib/sqlsync-solid-js/test/react-sanity.html b/lib/sqlsync-solid-js/test/solid-sanity.html
similarity index 52%
rename from lib/sqlsync-solid-js/test/react-sanity.html
rename to lib/sqlsync-solid-js/test/solid-sanity.html
index b7240f2..74c12f0 100644
--- a/lib/sqlsync-solid-js/test/react-sanity.html
+++ b/lib/sqlsync-solid-js/test/solid-sanity.html
@@ -1,15 +1,12 @@
-
+
-
-
+
- sqlsync-react tests
-
-
-
+ sqlsync-solid-js tests
+
+
if nothing renders, open the console
-
-
-
-
\ No newline at end of file
+
+
+