From 385b5d32a53ec067cadd1cb7fed508763ed71cb5 Mon Sep 17 00:00:00 2001 From: Aaron Boodman Date: Sun, 10 Oct 2021 06:15:49 -1000 Subject: [PATCH 1/8] Get rid of "Storage" abstraction and instead implement Replicache's WriteTransaction directly. This allows Replicache mutator definitions to be directly used in replicache-push.ts. --- backend/data.ts | 80 +++----------- backend/write-transaction-impl.ts | 68 ++++++++++++ frontend/data.ts | 109 ++----------------- frontend/smoothie.ts | 7 +- next-env.d.ts | 3 + pages/api/replicache-pull.ts | 17 ++- pages/api/replicache-push.ts | 175 +++++------------------------- pages/d/[id].tsx | 3 +- shared/client-state.ts | 40 +++---- {backend => shared}/decode.ts | 0 shared/mutators.ts | 86 +++++++++++++++ shared/shape.ts | 49 +++++---- shared/storage.ts | 13 --- 13 files changed, 267 insertions(+), 383 deletions(-) create mode 100644 backend/write-transaction-impl.ts rename {backend => shared}/decode.ts (100%) create mode 100644 shared/mutators.ts delete mode 100644 shared/storage.ts diff --git a/backend/data.ts b/backend/data.ts index faab783..7793d83 100644 --- a/backend/data.ts +++ b/backend/data.ts @@ -43,11 +43,11 @@ export async function setLastMutationID( ); } -export async function getObject( +export async function getObject( executor: ExecuteStatementFn, documentID: string, key: string -): Promise { +): Promise { const { records } = await executor( "SELECT V FROM Object WHERE DocumentID =:docID AND K = :key AND Deleted = False", { @@ -57,7 +57,7 @@ export async function getObject( ); const value = records?.[0]?.[0]?.stringValue; if (!value) { - return null; + return undefined; } return JSON.parse(value); } @@ -68,15 +68,18 @@ export async function putObject( key: string, value: JSONValue ): Promise { - await executor(` + await executor( + ` INSERT INTO Object (DocumentID, K, V, Deleted) VALUES (:docID, :key, :value, False) ON DUPLICATE KEY UPDATE V = :value, Deleted = False - `, { + `, + { docID: { stringValue: docID }, key: { stringValue: key }, value: { stringValue: JSON.stringify(value) }, - }); + } + ); } export async function delObject( @@ -84,63 +87,14 @@ export async function delObject( docID: string, key: string ): Promise { - await executor(` + await executor( + ` UPDATE Object SET Deleted = True WHERE DocumentID = :docID AND K = :key - `, { - docID: { stringValue: docID }, - key: { stringValue: key }, - }); -} - -export async function delAllShapes( - executor: ExecuteStatementFn, - docID: string -): Promise { - await executor(` - UPDATE Object Set Deleted = True - WHERE - DocumentID = :docID AND - K like 'shape-%' - `, { - docID: { stringValue: docID }, - }); -} - -export function storage(executor: ExecuteStatementFn, docID: string) { - // TODO: When we have the real mysql client, check whether it appears to do - // this caching internally. - const cache: { - [key: string]: { value: JSONValue | undefined; dirty: boolean }; - } = {}; - return { - getObject: async (key: string) => { - const entry = cache[key]; - if (entry) { - return entry.value; - } - const value = await getObject(executor, docID, key); - cache[key] = { value, dirty: false }; - return value; - }, - putObject: async (key: string, value: JSONValue) => { - cache[key] = { value, dirty: true }; - }, - delObject: async (key: string) => { - cache[key] = { value: undefined, dirty: true }; - }, - flush: async () => { - await Promise.all( - Object.entries(cache) - .filter(([, { dirty }]) => dirty) - .map(([k, { value }]) => { - if (value === undefined) { - return delObject(executor, docID, k); - } else { - return putObject(executor, docID, k, value); - } - }) - ); - }, - }; + `, + { + docID: { stringValue: docID }, + key: { stringValue: key }, + } + ); } diff --git a/backend/write-transaction-impl.ts b/backend/write-transaction-impl.ts new file mode 100644 index 0000000..8fc8253 --- /dev/null +++ b/backend/write-transaction-impl.ts @@ -0,0 +1,68 @@ +import type { JSONValue, ScanResult, WriteTransaction } from "replicache"; +import { delObject, getObject, putObject } from "./data"; +import { ExecuteStatementFn, transact } from "./rds"; + +/** + * Implements ReplicaCache's WriteTransaction interface in terms of a MySQL + * transaction. + */ +export class WriteTransactionImpl implements WriteTransaction { + private _docID: string; + private _executor: ExecuteStatementFn; + private _cache: Record< + string, + { value: JSONValue | undefined; dirty: boolean } + > = {}; + + constructor(executor: ExecuteStatementFn, docID: string) { + this._docID = docID; + this._executor = executor; + } + + async put(key: string, value: JSONValue): Promise { + this._cache[key] = { value, dirty: true }; + } + async del(key: string): Promise { + const had = await this.has(key); + this._cache[key] = { value: undefined, dirty: true }; + return had; + } + async get(key: string): Promise { + const entry = this._cache[key]; + if (entry) { + return entry.value; + } + const value = await getObject(this._executor, this._docID, key); + this._cache[key] = { value, dirty: false }; + return value; + } + async has(key: string): Promise { + const val = await this.get(key); + return val !== undefined; + } + + // TODO! + async isEmpty(): Promise { + throw new Error("not implemented"); + } + scan(): ScanResult { + throw new Error("not implemented"); + } + scanAll(): Promise<[string, JSONValue][]> { + throw new Error("not implemented"); + } + + async flush(): Promise { + await Promise.all( + Object.entries(this._cache) + .filter(([, { dirty }]) => dirty) + .map(([k, { value }]) => { + if (value === undefined) { + return delObject(this._executor, this._docID, k); + } else { + return putObject(this._executor, this._docID, k, value); + } + }) + ); + } +} diff --git a/frontend/data.ts b/frontend/data.ts index 1ac8229..b3f3a90 100644 --- a/frontend/data.ts +++ b/frontend/data.ts @@ -1,27 +1,10 @@ -import { Replicache, ReadTransaction, WriteTransaction } from "replicache"; +import { Replicache, ReadTransaction } from "replicache"; import type { JSONValue } from "replicache"; import { useSubscribe } from "replicache-react"; -import { - getShape, - Shape, - putShape, - moveShape, - resizeShape, - rotateShape, - deleteShape, - randomShape, - initShapes, -} from "../shared/shape"; -import { - getClientState, - overShape, - initClientState, - setCursor, - keyPrefix as clientStatePrefix, - selectShape, -} from "../shared/client-state"; -import type { ReadStorage, WriteStorage } from "../shared/storage"; +import { getShape } from "../shared/shape"; +import { getClientState, clientStatePrefix } from "../shared/client-state"; import type { UserInfo } from "../shared/client-state"; +import { mutators } from "../shared/mutators"; /** * Abstracts Replicache storage (key/value pairs) to entities (Shape). @@ -65,22 +48,22 @@ export async function createData( useShapeByID: (id: string) => subscribe(null, (tx: ReadTransaction) => { - return getShape(readStorage(tx), id); + return getShape(tx, id); }), useUserInfo: (clientID: string) => subscribe(null, async (tx: ReadTransaction) => { - return (await getClientState(readStorage(tx), clientID)).userInfo; + return (await getClientState(tx, clientID)).userInfo; }), useOverShapeID: () => subscribe("", async (tx: ReadTransaction) => { - return (await getClientState(readStorage(tx), clientID)).overID; + return (await getClientState(tx, clientID)).overID; }), useSelectedShapeID: () => subscribe("", async (tx: ReadTransaction) => { - return (await getClientState(readStorage(tx), clientID)).selectedID; + return (await getClientState(tx, clientID)).selectedID; }), useCollaboratorIDs: (clientID: string) => @@ -96,81 +79,7 @@ export async function createData( useClientInfo: (clientID: string) => subscribe(null, async (tx: ReadTransaction) => { - return await getClientState(readStorage(tx), clientID); + return await getClientState(tx, clientID); }), }; } - -export const mutators = { - async createShape(tx: WriteTransaction, args: { id: string; shape: Shape }) { - await putShape(writeStorage(tx), args); - }, - - async deleteShape(tx: WriteTransaction, id: string) { - await deleteShape(writeStorage(tx), id); - }, - - async moveShape( - tx: WriteTransaction, - args: { id: string; dx: number; dy: number } - ) { - await moveShape(writeStorage(tx), args); - }, - - async resizeShape(tx: WriteTransaction, args: { id: string; ds: number }) { - await resizeShape(writeStorage(tx), args); - }, - - async rotateShape(tx: WriteTransaction, args: { id: string; ddeg: number }) { - await rotateShape(writeStorage(tx), args); - }, - - async initClientState( - tx: WriteTransaction, - args: { id: string; defaultUserInfo: UserInfo } - ) { - await initClientState(writeStorage(tx), args); - }, - - async setCursor( - tx: WriteTransaction, - args: { id: string; x: number; y: number } - ) { - await setCursor(writeStorage(tx), args); - }, - - async overShape( - tx: WriteTransaction, - args: { clientID: string; shapeID: string } - ) { - await overShape(writeStorage(tx), args); - }, - - async selectShape( - tx: WriteTransaction, - args: { clientID: string; shapeID: string } - ) { - await selectShape(writeStorage(tx), args); - }, - - async deleteAllShapes(tx: WriteTransaction) { - await Promise.all( - (await tx.scan({ prefix: `shape-` }).keys().toArray()).map((k) => - tx.del(k) - ) - ); - }, -}; - -export function readStorage(tx: ReadTransaction): ReadStorage { - return { - getObject: (key: string) => tx.get(key), - }; -} - -function writeStorage(tx: WriteTransaction): WriteStorage { - return Object.assign(readStorage(tx), { - putObject: (key: string, value: JSONValue) => tx.put(key, value), - delObject: async (key: string) => void (await tx.del(key)), - }); -} diff --git a/frontend/smoothie.ts b/frontend/smoothie.ts index b2bd4bb..4ca1b53 100644 --- a/frontend/smoothie.ts +++ b/frontend/smoothie.ts @@ -3,7 +3,6 @@ import { useEffect, useState } from "react"; import { Replicache, ReadTransaction } from "replicache"; import { getClientState } from "../shared/client-state"; import { getShape } from "../shared/shape"; -import { readStorage } from "./data"; /** * Gets the current position of the cursor for `clientID`, but smoothing out @@ -18,8 +17,7 @@ export function useCursor( rep, `cursor/${clientID}`, async (tx: ReadTransaction) => { - const storage = readStorage(tx); - const clientState = await getClientState(storage, clientID); + const clientState = await getClientState(tx, clientID); return [clientState.cursor.x, clientState.cursor.y]; } ); @@ -41,8 +39,7 @@ export function useShape(rep: Replicache, shapeID: string) { rep, `shape/${shapeID}`, async (tx: ReadTransaction) => { - const storage = readStorage(tx); - const shape = await getShape(storage, shapeID); + const shape = await getShape(tx, shapeID); return ( shape && [shape.x, shape.y, shape.width, shape.height, shape.rotate] ); diff --git a/next-env.d.ts b/next-env.d.ts index c6643fd..9bc3dd4 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,3 +1,6 @@ /// /// /// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/pages/api/replicache-pull.ts b/pages/api/replicache-pull.ts index 2f39efa..d02a20b 100644 --- a/pages/api/replicache-pull.ts +++ b/pages/api/replicache-pull.ts @@ -2,13 +2,10 @@ import * as t from "io-ts"; import type { NextApiRequest, NextApiResponse } from "next"; import { ExecuteStatementCommandOutput, Field } from "@aws-sdk/client-rds-data"; import { transact } from "../../backend/rds"; -import { - getCookie, - getLastMutationID, - storage, -} from "../../backend/data"; -import { must } from "../../backend/decode"; +import { getCookie, getLastMutationID, storage } from "../../backend/data"; +import { must } from "../../shared/decode"; import { initShapes, randomShape } from "../../shared/shape"; +import { WriteTransactionImpl } from "../../backend/write-transaction-impl"; export default async (req: NextApiRequest, res: NextApiResponse) => { console.log(`Processing pull`, JSON.stringify(req.body, null, "")); @@ -23,12 +20,12 @@ export default async (req: NextApiRequest, res: NextApiResponse) => { let lastMutationID = 0; await transact(async (executor) => { - const s = storage(executor, docID); + const tx = new WriteTransactionImpl(executor, docID); await initShapes( - s, + tx, new Array(5).fill(null).map(() => randomShape()) ); - await s.flush(); + await tx.flush(); [entries, lastMutationID, responseCookie] = await Promise.all([ executor( `SELECT K, V, Deleted FROM Object @@ -47,7 +44,7 @@ export default async (req: NextApiRequest, res: NextApiResponse) => { // Grump. Typescript seems to not understand that the argument to transact() // is guaranteed to have been called before transact() exits. - entries = (entries as any) as ExecuteStatementCommandOutput; + entries = entries as any as ExecuteStatementCommandOutput; const resp: PullResponse = { lastMutationID, diff --git a/pages/api/replicache-push.ts b/pages/api/replicache-push.ts index 590a21e..f8bb5af 100644 --- a/pages/api/replicache-push.ts +++ b/pages/api/replicache-push.ts @@ -1,126 +1,25 @@ import * as t from "io-ts"; -import { ExecuteStatementFn, transact } from "../../backend/rds"; -import { - putShape, - moveShape, - resizeShape, - rotateShape, - shape, - deleteShape, - initShapes, -} from "../../shared/shape"; -import { - initClientState, - overShape, - selectShape, - setCursor, - userInfo, -} from "../../shared/client-state"; -import { - delAllShapes, - getLastMutationID, - setLastMutationID, - storage, -} from "../../backend/data"; -import { must } from "../../backend/decode"; +import { transact } from "../../backend/rds"; +import { getLastMutationID, setLastMutationID } from "../../backend/data"; import Pusher from "pusher"; import type { NextApiRequest, NextApiResponse } from "next"; - -const mutation = t.union([ - t.type({ - id: t.number, - name: t.literal("createShape"), - args: t.type({ - id: t.string, - shape, - }), - }), - t.type({ - id: t.number, - name: t.literal("deleteShape"), - args: t.string, - }), - t.type({ - id: t.number, - name: t.literal("moveShape"), - args: t.type({ - id: t.string, - dx: t.number, - dy: t.number, - }), - }), - t.type({ - id: t.number, - name: t.literal("resizeShape"), - args: t.type({ - id: t.string, - ds: t.number, - }), - }), - t.type({ - id: t.number, - name: t.literal("rotateShape"), - args: t.type({ - id: t.string, - ddeg: t.number, - }), - }), - t.type({ - id: t.number, - name: t.literal("initShapes"), - args: t.array( - t.type({ - id: t.string, - shape, - }) - ), - }), - t.type({ - id: t.number, - name: t.literal("initClientState"), - args: t.type({ - id: t.string, - defaultUserInfo: userInfo, - }), - }), - t.type({ - id: t.number, - name: t.literal("setCursor"), - args: t.type({ - id: t.string, - x: t.number, - y: t.number, - }), - }), - t.type({ - id: t.number, - name: t.literal("overShape"), - args: t.type({ - clientID: t.string, - shapeID: t.string, - }), - }), - t.type({ - id: t.number, - name: t.literal("selectShape"), - args: t.type({ - clientID: t.string, - shapeID: t.string, - }), - }), - t.type({ - id: t.number, - name: t.literal("deleteAllShapes"), - }), -]); +import { WriteTransactionImpl } from "../../backend/write-transaction-impl"; +import { mutators } from "../../shared/mutators"; +import { must } from "../../shared/decode"; + +// TODO: Either generate schema from mutator types, or vice versa, to tighten this. +// See notes in bug: https://github.com/rocicorp/replidraw/issues/47 +const mutation = t.type({ + id: t.number, + name: t.string, + args: t.any, +}); const pushRequest = t.type({ clientID: t.string, mutations: t.array(mutation), }); -type Mutation = t.TypeOf; - export default async (req: NextApiRequest, res: NextApiResponse) => { console.log("Processing push", JSON.stringify(req.body, null, "")); @@ -160,7 +59,7 @@ export default async (req: NextApiRequest, res: NextApiResponse) => { const t0 = Date.now(); await transact(async (executor) => { - const s = storage(executor, docID); + const tx = new WriteTransactionImpl(executor, docID); let lastMutationID = await getLastMutationID(executor, push.clientID); console.log("lastMutationID:", lastMutationID); @@ -183,39 +82,17 @@ export default async (req: NextApiRequest, res: NextApiResponse) => { console.log("Processing mutation:", JSON.stringify(mutation, null, "")); const t1 = Date.now(); - switch (mutation.name) { - case "createShape": - await putShape(s, mutation.args); - break; - case "deleteShape": - await deleteShape(s, mutation.args); - break; - case "moveShape": - await moveShape(s, mutation.args); - break; - case "resizeShape": - await resizeShape(s, mutation.args); - break; - case "rotateShape": - await rotateShape(s, mutation.args); - break; - case "initShapes": - await initShapes(s, mutation.args); - break; - case "initClientState": - await initClientState(s, mutation.args); - break; - case "setCursor": - await setCursor(s, mutation.args); - break; - case "overShape": - await overShape(s, mutation.args); - break; - case "selectShape": - await selectShape(s, mutation.args); - break; - case "deleteAllShapes": - await delAllShapes(executor, docID); + const mutator = (mutators as any)[mutation.name]; + if (!mutator) { + console.error(`Unknown mutator: ${mutation.name} - skipping`); + } + + try { + await mutator(tx, mutation.args); + } catch (e) { + console.error( + `Error executing mutator: ${JSON.stringify(mutator)}: ${e.message}` + ); } lastMutationID = expectedMutationID; @@ -223,8 +100,8 @@ export default async (req: NextApiRequest, res: NextApiResponse) => { } await Promise.all([ - s.flush(), setLastMutationID(executor, push.clientID, lastMutationID), + tx.flush(), ]); }); diff --git a/pages/d/[id].tsx b/pages/d/[id].tsx index 5db6aef..935146f 100644 --- a/pages/d/[id].tsx +++ b/pages/d/[id].tsx @@ -1,9 +1,10 @@ import { useEffect, useState } from "react"; import { Replicache } from "replicache"; -import { createData, mutators } from "../../frontend/data"; +import { createData } from "../../frontend/data"; import { Designer } from "../../frontend/designer"; import { Nav } from "../../frontend/nav"; import Pusher from "pusher-js"; +import { mutators } from "../../shared/mutators"; import type { Data } from "../../frontend/data"; import { randUserInfo } from "../../shared/client-state"; diff --git a/shared/client-state.ts b/shared/client-state.ts index fa4bfbe..5dd467e 100644 --- a/shared/client-state.ts +++ b/shared/client-state.ts @@ -1,6 +1,6 @@ +import { ReadTransaction, WriteTransaction } from "replicache"; import * as t from "io-ts"; -import { must } from "../backend/decode"; -import { ReadStorage, WriteStorage } from "./storage"; +import { must } from "./decode"; import { randInt } from "./rand"; const colors = [ @@ -56,13 +56,13 @@ export type UserInfo = t.TypeOf; export type ClientState = t.TypeOf; export async function initClientState( - storage: WriteStorage, + tx: WriteTransaction, { id, defaultUserInfo }: { id: string; defaultUserInfo: UserInfo } ): Promise { - if (await storage.getObject(key(id))) { + if (await tx.has(key(id))) { return; } - await putClientState(storage, { + await putClientState(tx, { id, clientState: { cursor: { @@ -77,10 +77,10 @@ export async function initClientState( } export async function getClientState( - storage: ReadStorage, + tx: ReadTransaction, id: string ): Promise { - const jv = await storage.getObject(key(id)); + const jv = await tx.get(key(id)); if (!jv) { throw new Error("Expected clientState to be initialized already: " + id); } @@ -88,38 +88,38 @@ export async function getClientState( } export function putClientState( - storage: WriteStorage, + tx: WriteTransaction, { id, clientState }: { id: string; clientState: ClientState } ): Promise { - return storage.putObject(key(id), clientState); + return tx.put(key(id), clientState); } export async function setCursor( - storage: WriteStorage, + tx: WriteTransaction, { id, x, y }: { id: string; x: number; y: number } ): Promise { - const clientState = await getClientState(storage, id); + const clientState = await getClientState(tx, id); clientState.cursor.x = x; clientState.cursor.y = y; - await putClientState(storage, { id, clientState }); + await putClientState(tx, { id, clientState }); } export async function overShape( - storage: WriteStorage, + tx: WriteTransaction, { clientID, shapeID }: { clientID: string; shapeID: string } ): Promise { - const client = await getClientState(storage, clientID); + const client = await getClientState(tx, clientID); client.overID = shapeID; - await putClientState(storage, { id: clientID, clientState: client }); + await putClientState(tx, { id: clientID, clientState: client }); } export async function selectShape( - storage: WriteStorage, + tx: WriteTransaction, { clientID, shapeID }: { clientID: string; shapeID: string } ): Promise { - const client = await getClientState(storage, clientID); + const client = await getClientState(tx, clientID); client.selectedID = shapeID; - await putClientState(storage, { id: clientID, clientState: client }); + await putClientState(tx, { id: clientID, clientState: client }); } export function randUserInfo(): UserInfo { @@ -132,7 +132,7 @@ export function randUserInfo(): UserInfo { } function key(id: string): string { - return `${keyPrefix}${id}`; + return `${clientStatePrefix}${id}`; } -export const keyPrefix = `client-state-`; +export const clientStatePrefix = `client-state-`; diff --git a/backend/decode.ts b/shared/decode.ts similarity index 100% rename from backend/decode.ts rename to shared/decode.ts diff --git a/shared/mutators.ts b/shared/mutators.ts new file mode 100644 index 0000000..39259b9 --- /dev/null +++ b/shared/mutators.ts @@ -0,0 +1,86 @@ +import { WriteTransaction } from "replicache"; +import { + initClientState, + setCursor, + overShape, + selectShape, + UserInfo, +} from "./client-state"; +import { + Shape, + putShape, + deleteShape, + moveShape, + resizeShape, + rotateShape, + initShapes, + shapePrefix as shapePrefix, +} from "./shape"; + +export const mutators = { + async createShape(tx: WriteTransaction, args: { id: string; shape: Shape }) { + await putShape(tx, args); + }, + + async deleteShape(tx: WriteTransaction, id: string) { + await deleteShape(tx, id); + }, + + async moveShape( + tx: WriteTransaction, + args: { id: string; dx: number; dy: number } + ) { + await moveShape(tx, args); + }, + + async resizeShape(tx: WriteTransaction, args: { id: string; ds: number }) { + await resizeShape(tx, args); + }, + + async rotateShape(tx: WriteTransaction, args: { id: string; ddeg: number }) { + await rotateShape(tx, args); + }, + + async initClientState( + tx: WriteTransaction, + args: { id: string; defaultUserInfo: UserInfo } + ) { + await initClientState(tx, args); + }, + + async setCursor( + tx: WriteTransaction, + args: { id: string; x: number; y: number } + ) { + await setCursor(tx, args); + }, + + async overShape( + tx: WriteTransaction, + args: { clientID: string; shapeID: string } + ) { + await overShape(tx, args); + }, + + async selectShape( + tx: WriteTransaction, + args: { clientID: string; shapeID: string } + ) { + await selectShape(tx, args); + }, + + async deleteAllShapes(tx: WriteTransaction) { + await Promise.all( + ( + await tx.scan({ prefix: shapePrefix }).keys().toArray() + ).map((k) => tx.del(k)) + ); + }, + + async initShapes( + tx: WriteTransaction, + shapes: { id: string; shape: Shape }[] + ) { + await initShapes(tx, shapes); + }, +}; diff --git a/shared/shape.ts b/shared/shape.ts index 406267f..ee5b734 100644 --- a/shared/shape.ts +++ b/shared/shape.ts @@ -1,8 +1,8 @@ +import { ReadTransaction, WriteTransaction } from "replicache"; import * as t from "io-ts"; -import { must } from "../backend/decode"; +import { must } from "./decode"; import { nanoid } from "nanoid"; import { randInt } from "./rand"; -import { ReadStorage, WriteStorage } from "./storage"; export const shape = t.type({ type: t.literal("rect"), @@ -17,10 +17,10 @@ export const shape = t.type({ export type Shape = t.TypeOf; export async function getShape( - storage: ReadStorage, + tx: ReadTransaction, id: string ): Promise { - const jv = await storage.getObject(key(id)); + const jv = await tx.get(key(id)); if (!jv) { console.log(`Specified shape ${id} not found.`); return null; @@ -29,33 +29,36 @@ export async function getShape( } export function putShape( - storage: WriteStorage, + tx: WriteTransaction, { id, shape }: { id: string; shape: Shape } ): Promise { - return storage.putObject(key(id), shape); + return tx.put(key(id), shape); } -export function deleteShape(storage: WriteStorage, id: string): Promise { - return storage.delObject(key(id)); +export async function deleteShape( + tx: WriteTransaction, + id: string +): Promise { + await tx.del(key(id)); } export async function moveShape( - storage: WriteStorage, + tx: WriteTransaction, { id, dx, dy }: { id: string; dx: number; dy: number } ): Promise { - const shape = await getShape(storage, id); + const shape = await getShape(tx, id); if (shape) { shape.x += dx; shape.y += dy; - await putShape(storage, { id, shape }); + await putShape(tx, { id, shape }); } } export async function resizeShape( - storage: WriteStorage, + tx: WriteTransaction, { id, ds }: { id: string; ds: number } ): Promise { - const shape = await getShape(storage, id); + const shape = await getShape(tx, id); if (shape) { const minSize = 10; const dw = Math.max(minSize - shape.width, ds); @@ -64,38 +67,40 @@ export async function resizeShape( shape.height += dh; shape.x -= dw / 2; shape.y -= dh / 2; - await putShape(storage, { id, shape }); + await putShape(tx, { id, shape }); } } export async function rotateShape( - storage: WriteStorage, + tx: WriteTransaction, { id, ddeg }: { id: string; ddeg: number } ): Promise { - const shape = await getShape(storage, id); + const shape = await getShape(tx, id); if (shape) { shape.rotate += ddeg; - await putShape(storage, { id, shape }); + await putShape(tx, { id, shape }); } } export async function initShapes( - storage: WriteStorage, + tx: WriteTransaction, shapes: { id: string; shape: Shape }[] ) { - if (await storage.getObject("initialized")) { + if (await tx.has("initialized")) { return; } await Promise.all([ - storage.putObject("initialized", true), - ...shapes.map((s) => putShape(storage, s)), + tx.put("initialized", true), + ...shapes.map((s) => putShape(tx, s)), ]); } function key(id: string): string { - return `shape-${id}`; + return `${shapePrefix}${id}`; } +export const shapePrefix = "shape-"; + const colors = ["red", "blue", "white", "green", "yellow"]; let nextColor = 0; diff --git a/shared/storage.ts b/shared/storage.ts deleted file mode 100644 index a92593e..0000000 --- a/shared/storage.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { JSONValue, ReadonlyJSONValue } from "replicache"; - -/** - * Interface required of underlying storage. - */ -export interface ReadStorage { - getObject(key: string): Promise; -} - -export interface WriteStorage extends ReadStorage { - putObject(key: string, value: JSONValue): Promise; - delObject(key: string): Promise; -} From a20b92429b524e5177e8b0bae3d949ea0edf3edc Mon Sep 17 00:00:00 2001 From: Aaron Boodman Date: Sun, 10 Oct 2021 06:23:18 -1000 Subject: [PATCH 2/8] Move contents of "shared" directory into "frontend". We don't really have a "shared" concept anymore, because all the frontend types are directly used by backend! --- {shared => frontend}/client-state.ts | 0 frontend/data.ts | 8 ++++---- {shared => frontend}/decode.ts | 0 {shared => frontend}/mutators.ts | 0 frontend/nav.tsx | 2 +- {shared => frontend}/rand.ts | 0 {shared => frontend}/shape.ts | 0 frontend/smoothie.ts | 4 ++-- pages/api/replicache-pull.ts | 6 +++--- pages/api/replicache-push.ts | 4 ++-- pages/d/[id].tsx | 6 ++---- 11 files changed, 14 insertions(+), 16 deletions(-) rename {shared => frontend}/client-state.ts (100%) rename {shared => frontend}/decode.ts (100%) rename {shared => frontend}/mutators.ts (100%) rename {shared => frontend}/rand.ts (100%) rename {shared => frontend}/shape.ts (100%) diff --git a/shared/client-state.ts b/frontend/client-state.ts similarity index 100% rename from shared/client-state.ts rename to frontend/client-state.ts diff --git a/frontend/data.ts b/frontend/data.ts index b3f3a90..6980963 100644 --- a/frontend/data.ts +++ b/frontend/data.ts @@ -1,10 +1,10 @@ import { Replicache, ReadTransaction } from "replicache"; import type { JSONValue } from "replicache"; import { useSubscribe } from "replicache-react"; -import { getShape } from "../shared/shape"; -import { getClientState, clientStatePrefix } from "../shared/client-state"; -import type { UserInfo } from "../shared/client-state"; -import { mutators } from "../shared/mutators"; +import { getShape } from "../frontend/shape"; +import { getClientState, clientStatePrefix } from "../frontend/client-state"; +import type { UserInfo } from "../frontend/client-state"; +import { mutators } from "../frontend/mutators"; /** * Abstracts Replicache storage (key/value pairs) to entities (Shape). diff --git a/shared/decode.ts b/frontend/decode.ts similarity index 100% rename from shared/decode.ts rename to frontend/decode.ts diff --git a/shared/mutators.ts b/frontend/mutators.ts similarity index 100% rename from shared/mutators.ts rename to frontend/mutators.ts diff --git a/frontend/nav.tsx b/frontend/nav.tsx index 5848955..4a66a0f 100644 --- a/frontend/nav.tsx +++ b/frontend/nav.tsx @@ -1,7 +1,7 @@ import { useEffect, useRef, useState } from "react"; import styles from "./nav.module.css"; import { Data } from "./data"; -import { randomShape } from "../shared/shape"; +import { randomShape } from "../frontend/shape"; import Modal from "react-bootstrap/Modal"; import Button from "react-bootstrap/Button"; import Form from "react-bootstrap/Form"; diff --git a/shared/rand.ts b/frontend/rand.ts similarity index 100% rename from shared/rand.ts rename to frontend/rand.ts diff --git a/shared/shape.ts b/frontend/shape.ts similarity index 100% rename from shared/shape.ts rename to frontend/shape.ts diff --git a/frontend/smoothie.ts b/frontend/smoothie.ts index 4ca1b53..6ac6120 100644 --- a/frontend/smoothie.ts +++ b/frontend/smoothie.ts @@ -1,8 +1,8 @@ import hermite from "cubic-hermite"; import { useEffect, useState } from "react"; import { Replicache, ReadTransaction } from "replicache"; -import { getClientState } from "../shared/client-state"; -import { getShape } from "../shared/shape"; +import { getClientState } from "../frontend/client-state"; +import { getShape } from "../frontend/shape"; /** * Gets the current position of the cursor for `clientID`, but smoothing out diff --git a/pages/api/replicache-pull.ts b/pages/api/replicache-pull.ts index d02a20b..276bb76 100644 --- a/pages/api/replicache-pull.ts +++ b/pages/api/replicache-pull.ts @@ -2,9 +2,9 @@ import * as t from "io-ts"; import type { NextApiRequest, NextApiResponse } from "next"; import { ExecuteStatementCommandOutput, Field } from "@aws-sdk/client-rds-data"; import { transact } from "../../backend/rds"; -import { getCookie, getLastMutationID, storage } from "../../backend/data"; -import { must } from "../../shared/decode"; -import { initShapes, randomShape } from "../../shared/shape"; +import { getCookie, getLastMutationID } from "../../backend/data"; +import { must } from "../../frontend/decode"; +import { initShapes, randomShape } from "../../frontend/shape"; import { WriteTransactionImpl } from "../../backend/write-transaction-impl"; export default async (req: NextApiRequest, res: NextApiResponse) => { diff --git a/pages/api/replicache-push.ts b/pages/api/replicache-push.ts index f8bb5af..6e532b6 100644 --- a/pages/api/replicache-push.ts +++ b/pages/api/replicache-push.ts @@ -4,8 +4,8 @@ import { getLastMutationID, setLastMutationID } from "../../backend/data"; import Pusher from "pusher"; import type { NextApiRequest, NextApiResponse } from "next"; import { WriteTransactionImpl } from "../../backend/write-transaction-impl"; -import { mutators } from "../../shared/mutators"; -import { must } from "../../shared/decode"; +import { mutators } from "../../frontend/mutators"; +import { must } from "../../frontend/decode"; // TODO: Either generate schema from mutator types, or vice versa, to tighten this. // See notes in bug: https://github.com/rocicorp/replidraw/issues/47 diff --git a/pages/d/[id].tsx b/pages/d/[id].tsx index 935146f..909b3d8 100644 --- a/pages/d/[id].tsx +++ b/pages/d/[id].tsx @@ -4,10 +4,9 @@ import { createData } from "../../frontend/data"; import { Designer } from "../../frontend/designer"; import { Nav } from "../../frontend/nav"; import Pusher from "pusher-js"; -import { mutators } from "../../shared/mutators"; - +import { mutators } from "../../frontend/mutators"; import type { Data } from "../../frontend/data"; -import { randUserInfo } from "../../shared/client-state"; +import { randUserInfo } from "../../frontend/client-state"; export default function Home() { const [data, setData] = useState(null); @@ -20,7 +19,6 @@ export default function Home() { } const [, , docID] = location.pathname.split("/"); - const isProd = location.host.indexOf(".vercel.app") > -1; const rep = new Replicache({ pushURL: `/api/replicache-push?docID=${docID}`, pullURL: `/api/replicache-pull?docID=${docID}`, From 7f327d9f73a7968d4faa197f572531fbc76135de Mon Sep 17 00:00:00 2001 From: Aaron Boodman Date: Sun, 10 Oct 2021 06:37:04 -1000 Subject: [PATCH 3/8] Factor out subscriptons.ts for symetry with mutations.js. --- frontend/collaborator.tsx | 3 +- frontend/data.ts | 58 +----------------------- frontend/declarations.d.ts | 3 -- frontend/designer.tsx | 16 ++++--- frontend/nav.tsx | 3 +- frontend/rect-controller.tsx | 3 +- frontend/rect.tsx | 3 +- frontend/subscriptions.ts | 86 ++++++++++++++++++++++++++++++++++++ 8 files changed, 107 insertions(+), 68 deletions(-) create mode 100644 frontend/subscriptions.ts diff --git a/frontend/collaborator.tsx b/frontend/collaborator.tsx index a5c0676..ef50ca6 100644 --- a/frontend/collaborator.tsx +++ b/frontend/collaborator.tsx @@ -3,6 +3,7 @@ import styles from "./collaborator.module.css"; import { useEffect, useState } from "react"; import { Rect } from "./rect"; import { useCursor } from "./smoothie"; +import { useClientInfo } from "./subscriptions"; const hideCollaboratorDelay = 5000; @@ -21,7 +22,7 @@ export function Collaborator({ data: Data; clientID: string; }) { - const clientInfo = data.useClientInfo(clientID); + const clientInfo = useClientInfo(data.rep, data.clientID); const [lastPos, setLastPos] = useState(null); const [gotFirstChange, setGotFirstChange] = useState(false); const [, setPoke] = useState({}); diff --git a/frontend/data.ts b/frontend/data.ts index 6980963..09feb77 100644 --- a/frontend/data.ts +++ b/frontend/data.ts @@ -1,8 +1,4 @@ -import { Replicache, ReadTransaction } from "replicache"; -import type { JSONValue } from "replicache"; -import { useSubscribe } from "replicache-react"; -import { getShape } from "../frontend/shape"; -import { getClientState, clientStatePrefix } from "../frontend/client-state"; +import { Replicache } from "replicache"; import type { UserInfo } from "../frontend/client-state"; import { mutators } from "../frontend/mutators"; @@ -18,13 +14,6 @@ export async function createData( ) { let clientID = await rep.clientID; - function subscribe( - def: T, - f: (tx: ReadTransaction) => Promise - ): T { - return useSubscribe(rep, f, def); - } - await rep.mutate.initClientState({ id: clientID, defaultUserInfo, @@ -33,53 +22,10 @@ export async function createData( return { clientID, - get rep(): Replicache { + get rep(): Replicache { return rep; }, ...rep.mutate, - - // subscriptions - useShapeIDs: () => - subscribe([], async (tx: ReadTransaction) => { - const shapes = await tx.scan({ prefix: "shape-" }).keys().toArray(); - return shapes.map((k) => k.split("-", 2)[1]); - }), - - useShapeByID: (id: string) => - subscribe(null, (tx: ReadTransaction) => { - return getShape(tx, id); - }), - - useUserInfo: (clientID: string) => - subscribe(null, async (tx: ReadTransaction) => { - return (await getClientState(tx, clientID)).userInfo; - }), - - useOverShapeID: () => - subscribe("", async (tx: ReadTransaction) => { - return (await getClientState(tx, clientID)).overID; - }), - - useSelectedShapeID: () => - subscribe("", async (tx: ReadTransaction) => { - return (await getClientState(tx, clientID)).selectedID; - }), - - useCollaboratorIDs: (clientID: string) => - subscribe([], async (tx: ReadTransaction) => { - const r = []; - for await (let k of tx.scan({ prefix: clientStatePrefix }).keys()) { - if (!k.endsWith(clientID)) { - r.push(k.substr(clientStatePrefix.length)); - } - } - return r; - }), - - useClientInfo: (clientID: string) => - subscribe(null, async (tx: ReadTransaction) => { - return await getClientState(tx, clientID); - }), }; } diff --git a/frontend/declarations.d.ts b/frontend/declarations.d.ts index fc9368f..8daaad5 100644 --- a/frontend/declarations.d.ts +++ b/frontend/declarations.d.ts @@ -1,4 +1 @@ -// TODO: Why isn't Typescript picking up the declaration in -// replicache-react? -declare module "replicache-react"; declare module "cubic-hermite"; diff --git a/frontend/designer.tsx b/frontend/designer.tsx index b4c56a4..7553a69 100644 --- a/frontend/designer.tsx +++ b/frontend/designer.tsx @@ -6,13 +6,19 @@ import { Collaborator } from "./collaborator"; import { RectController } from "./rect-controller"; import { touchToMouse } from "./events"; import { Selection } from "./selection"; -import { DraggableCore, DraggableEvent, DraggableData } from "react-draggable"; +import { DraggableCore } from "react-draggable"; +import { + useShapeIDs, + useOverShapeID, + useSelectedShapeID, + useCollaboratorIDs, +} from "./subscriptions"; export function Designer({ data }: { data: Data }) { - const ids = data.useShapeIDs(); - const overID = data.useOverShapeID(); - const selectedID = data.useSelectedShapeID(); - const collaboratorIDs = data.useCollaboratorIDs(data.clientID); + const ids = useShapeIDs(data.rep); + const overID = useOverShapeID(data.rep); + const selectedID = useSelectedShapeID(data.rep); + const collaboratorIDs = useCollaboratorIDs(data.rep); const ref = useRef(null); const [dragging, setDragging] = useState(false); diff --git a/frontend/nav.tsx b/frontend/nav.tsx index 4a66a0f..37800ff 100644 --- a/frontend/nav.tsx +++ b/frontend/nav.tsx @@ -5,12 +5,13 @@ import { randomShape } from "../frontend/shape"; import Modal from "react-bootstrap/Modal"; import Button from "react-bootstrap/Button"; import Form from "react-bootstrap/Form"; +import { useUserInfo } from "./subscriptions"; export function Nav({ data }: { data: Data }) { const [aboutVisible, showAbout] = useState(false); const [shareVisible, showShare] = useState(false); const urlBox = useRef(null); - const userInfo = data.useUserInfo(data.clientID); + const userInfo = useUserInfo(data.rep); useEffect(() => { if (shareVisible) { diff --git a/frontend/rect-controller.tsx b/frontend/rect-controller.tsx index aefd7e2..074439e 100644 --- a/frontend/rect-controller.tsx +++ b/frontend/rect-controller.tsx @@ -1,11 +1,12 @@ import { Data } from "./data"; import { Rect } from "./rect"; import { DraggableCore, DraggableEvent, DraggableData } from "react-draggable"; +import { useShapeByID } from "./subscriptions"; // TODO: In the future I imagine this becoming ShapeController and // there also be a Shape that wraps Rect and also knows how to draw Circle, etc. export function RectController({ data, id }: { data: Data; id: string }) { - const shape = data.useShapeByID(id); + const shape = useShapeByID(data.rep, id); const onMouseEnter = () => data.overShape({ clientID: data.clientID, shapeID: id }); diff --git a/frontend/rect.tsx b/frontend/rect.tsx index 07f452b..9d39db1 100644 --- a/frontend/rect.tsx +++ b/frontend/rect.tsx @@ -1,6 +1,7 @@ import React, { MouseEventHandler, TouchEventHandler } from "react"; import { Data } from "./data"; import { useShape } from "./smoothie"; +import { useShapeByID } from "./subscriptions"; export function Rect({ data, @@ -21,7 +22,7 @@ export function Rect({ onMouseEnter?: MouseEventHandler; onMouseLeave?: MouseEventHandler; }) { - const shape = data.useShapeByID(id); + const shape = useShapeByID(data.rep, id); const coords = useShape(data.rep, id); if (!shape || !coords) { return null; diff --git a/frontend/subscriptions.ts b/frontend/subscriptions.ts new file mode 100644 index 0000000..651f57e --- /dev/null +++ b/frontend/subscriptions.ts @@ -0,0 +1,86 @@ +import { useSubscribe } from "replicache-react"; +import { getClientState, clientStatePrefix } from "./client-state"; +import { getShape, shapePrefix } from "./shape"; +import { mutators } from "./mutators"; +import { Replicache } from "replicache"; + +export function useShapeIDs(rep: Replicache) { + return useSubscribe( + rep, + async (tx) => { + const shapes = await tx.scan({ prefix: shapePrefix }).keys().toArray(); + return shapes.map((k) => k.split("-", 2)[1]); + }, + [] + ); +} + +export function useShapeByID(rep: Replicache, id: string) { + return useSubscribe( + rep, + async (tx) => { + return await getShape(tx, id); + }, + null + ); +} + +export function useUserInfo(rep: Replicache) { + return useSubscribe( + rep, + async (tx) => { + return (await getClientState(tx, await rep.clientID)).userInfo; + }, + null + ); +} + +export function useOverShapeID(rep: Replicache) { + return useSubscribe( + rep, + async (tx) => { + return (await getClientState(tx, await rep.clientID)).overID; + }, + "" + ); +} + +export function useSelectedShapeID(rep: Replicache) { + return useSubscribe( + rep, + async (tx) => { + return (await getClientState(tx, await rep.clientID)).selectedID; + }, + "" + ); +} + +export function useCollaboratorIDs(rep: Replicache) { + return useSubscribe( + rep, + async (tx) => { + const clientIDs = await tx + .scan({ prefix: clientStatePrefix }) + .keys() + .toArray(); + const myClientID = await rep.clientID; + return clientIDs + .filter((k) => !k.endsWith(myClientID)) + .map((k) => k.substr(clientStatePrefix.length)); + }, + [] + ); +} + +export function useClientInfo( + rep: Replicache, + clientID: string +) { + return useSubscribe( + rep, + async (tx) => { + return await getClientState(tx, clientID); + }, + null + ); +} From d27f89fd5e57945212f97eac1d5127598a0adc57 Mon Sep 17 00:00:00 2001 From: Aaron Boodman Date: Sun, 10 Oct 2021 07:11:22 -1000 Subject: [PATCH 4/8] Get rid of Data helper class --- frontend/collaborator.tsx | 13 ++++++----- frontend/data.ts | 31 -------------------------- frontend/designer.tsx | 43 +++++++++++++++++++++--------------- frontend/mutators.ts | 2 ++ frontend/nav.tsx | 14 +++++------- frontend/rect-controller.tsx | 32 ++++++++++++++++++--------- frontend/rect.tsx | 11 ++++----- frontend/selection.tsx | 15 +++++++------ pages/d/[id].tsx | 25 +++++++++++---------- 9 files changed, 89 insertions(+), 97 deletions(-) delete mode 100644 frontend/data.ts diff --git a/frontend/collaborator.tsx b/frontend/collaborator.tsx index ef50ca6..aba2208 100644 --- a/frontend/collaborator.tsx +++ b/frontend/collaborator.tsx @@ -1,8 +1,9 @@ -import { Data } from "./data"; import styles from "./collaborator.module.css"; import { useEffect, useState } from "react"; import { Rect } from "./rect"; import { useCursor } from "./smoothie"; +import { Replicache } from "replicache"; +import { M } from "./mutators"; import { useClientInfo } from "./subscriptions"; const hideCollaboratorDelay = 5000; @@ -16,17 +17,17 @@ interface Position { } export function Collaborator({ - data, + rep, clientID, }: { - data: Data; + rep: Replicache; clientID: string; }) { - const clientInfo = useClientInfo(data.rep, data.clientID); + const clientInfo = useClientInfo(rep, clientID); const [lastPos, setLastPos] = useState(null); const [gotFirstChange, setGotFirstChange] = useState(false); const [, setPoke] = useState({}); - const cursor = useCursor(data.rep, clientID); + const cursor = useCursor(rep, clientID); let curPos = null; let userInfo = null; @@ -77,7 +78,7 @@ export function Collaborator({ {clientInfo.selectedID && ( >; -type Await = T extends PromiseLike ? U : T; - -export async function createData( - rep: Replicache, - defaultUserInfo: UserInfo -) { - let clientID = await rep.clientID; - - await rep.mutate.initClientState({ - id: clientID, - defaultUserInfo, - }); - - return { - clientID, - - get rep(): Replicache { - return rep; - }, - - ...rep.mutate, - }; -} diff --git a/frontend/designer.tsx b/frontend/designer.tsx index 7553a69..a6612bd 100644 --- a/frontend/designer.tsx +++ b/frontend/designer.tsx @@ -1,7 +1,6 @@ import React, { useRef, useState } from "react"; import { Rect } from "./rect"; import { HotKeys } from "react-hotkeys"; -import { Data } from "./data"; import { Collaborator } from "./collaborator"; import { RectController } from "./rect-controller"; import { touchToMouse } from "./events"; @@ -13,32 +12,40 @@ import { useSelectedShapeID, useCollaboratorIDs, } from "./subscriptions"; +import { Replicache } from "replicache"; +import { M } from "./mutators"; -export function Designer({ data }: { data: Data }) { - const ids = useShapeIDs(data.rep); - const overID = useOverShapeID(data.rep); - const selectedID = useSelectedShapeID(data.rep); - const collaboratorIDs = useCollaboratorIDs(data.rep); +export function Designer({ rep }: { rep: Replicache }) { + const ids = useShapeIDs(rep); + const overID = useOverShapeID(rep); + const selectedID = useSelectedShapeID(rep); + const collaboratorIDs = useCollaboratorIDs(rep); const ref = useRef(null); const [dragging, setDragging] = useState(false); const handlers = { - moveLeft: () => data.moveShape({ id: selectedID, dx: -20, dy: 0 }), - moveRight: () => data.moveShape({ id: selectedID, dx: 20, dy: 0 }), - moveUp: () => data.moveShape({ id: selectedID, dx: 0, dy: -20 }), - moveDown: () => data.moveShape({ id: selectedID, dx: 0, dy: 20 }), + moveLeft: () => rep.mutate.moveShape({ id: selectedID, dx: -20, dy: 0 }), + moveRight: () => rep.mutate.moveShape({ id: selectedID, dx: 20, dy: 0 }), + moveUp: () => rep.mutate.moveShape({ id: selectedID, dx: 0, dy: -20 }), + moveDown: () => rep.mutate.moveShape({ id: selectedID, dx: 0, dy: 20 }), deleteShape: () => { // Prevent navigating backward on some browsers. event?.preventDefault(); - data.deleteShape(selectedID); + rep.mutate.deleteShape(selectedID); }, }; - const onMouseMove = ({ pageX, pageY }: { pageX: number; pageY: number }) => { + const onMouseMove = async ({ + pageX, + pageY, + }: { + pageX: number; + pageY: number; + }) => { if (ref && ref.current) { - data.setCursor({ - id: data.clientID, + rep.mutate.setCursor({ + id: await rep.clientID, x: pageX, y: pageY - ref.current.offsetTop, }); @@ -75,7 +82,7 @@ export function Designer({ data }: { data: Data }) { @@ -87,7 +94,7 @@ export function Designer({ data }: { data: Data }) { diff --git a/frontend/mutators.ts b/frontend/mutators.ts index 39259b9..7cc1a42 100644 --- a/frontend/mutators.ts +++ b/frontend/mutators.ts @@ -17,6 +17,8 @@ import { shapePrefix as shapePrefix, } from "./shape"; +export type M = typeof mutators; + export const mutators = { async createShape(tx: WriteTransaction, args: { id: string; shape: Shape }) { await putShape(tx, args); diff --git a/frontend/nav.tsx b/frontend/nav.tsx index 37800ff..d483b62 100644 --- a/frontend/nav.tsx +++ b/frontend/nav.tsx @@ -1,17 +1,18 @@ import { useEffect, useRef, useState } from "react"; import styles from "./nav.module.css"; -import { Data } from "./data"; import { randomShape } from "../frontend/shape"; import Modal from "react-bootstrap/Modal"; import Button from "react-bootstrap/Button"; import Form from "react-bootstrap/Form"; import { useUserInfo } from "./subscriptions"; +import { Replicache } from "replicache"; +import { M } from "./mutators"; -export function Nav({ data }: { data: Data }) { +export function Nav({ rep }: { rep: Replicache }) { const [aboutVisible, showAbout] = useState(false); const [shareVisible, showShare] = useState(false); const urlBox = useRef(null); - const userInfo = useUserInfo(data.rep); + const userInfo = useUserInfo(rep); useEffect(() => { if (shareVisible) { @@ -20,10 +21,7 @@ export function Nav({ data }: { data: Data }) { }); const onRectangle = () => { - if (!data) { - return; - } - data.createShape(randomShape()); + rep.mutate.createShape(randomShape()); }; return ( @@ -50,7 +48,7 @@ export function Nav({ data }: { data: Data }) {
data?.deleteAllShapes()} + onClick={() => rep.mutate.deleteAllShapes()} > ; + id: string; +}) { + const shape = useShapeByID(rep, id); - const onMouseEnter = () => - data.overShape({ clientID: data.clientID, shapeID: id }); - const onMouseLeave = () => - data.overShape({ clientID: data.clientID, shapeID: "" }); + const onMouseEnter = async () => + rep.mutate.overShape({ clientID: await rep.clientID, shapeID: id }); + const onMouseLeave = async () => + rep.mutate.overShape({ clientID: await rep.clientID, shapeID: "" }); const onDragStart = (e: DraggableEvent, d: DraggableData) => { - data.selectShape({ clientID: data.clientID, shapeID: id }); + // Can't mark onDragStart async because it changes return type and onDragStart + // must return void. + const blech = async () => { + rep.mutate.selectShape({ clientID: await rep.clientID, shapeID: id }); + }; + blech(); }; const onDrag = (e: DraggableEvent, d: DraggableData) => { // This is subtle, and worth drawing attention to: @@ -24,7 +36,7 @@ export function RectController({ data, id }: { data: Data; id: string }) { // We will apply this movement to whatever the state happens to be when we // replay. If somebody else was moving the object at the same moment, we'll // then end up with a union of the two vectors, which is what we want! - data.moveShape({ + rep.mutate.moveShape({ id, dx: d.deltaX, dy: d.deltaY, @@ -40,7 +52,7 @@ export function RectController({ data, id }: { data: Data; id: string }) {
; id: string; highlight?: boolean; highlightColor?: string; @@ -22,8 +23,8 @@ export function Rect({ onMouseEnter?: MouseEventHandler; onMouseLeave?: MouseEventHandler; }) { - const shape = useShapeByID(data.rep, id); - const coords = useShape(data.rep, id); + const shape = useShapeByID(rep, id); + const coords = useShape(rep, id); if (!shape || !coords) { return null; } diff --git a/frontend/selection.tsx b/frontend/selection.tsx index a9b4330..a9246b3 100644 --- a/frontend/selection.tsx +++ b/frontend/selection.tsx @@ -1,18 +1,19 @@ -import { Data } from "./data"; import { Rect } from "./rect"; import { useShape } from "./smoothie"; import { DraggableCore, DraggableEvent, DraggableData } from "react-draggable"; +import { Replicache } from "replicache"; +import { M } from "./mutators"; export function Selection({ - data, + rep, id, containerOffsetTop, }: { - data: Data; + rep: Replicache; id: string; containerOffsetTop: number | null; }) { - const coords = useShape(data.rep, id); + const coords = useShape(rep, id); const gripSize = 19; const center = (coords: NonNullable>) => { @@ -43,7 +44,7 @@ export function Selection({ ); const s1 = size(shapeCenter.x, d.x, shapeCenter.y, d.y); - data.resizeShape({ id, ds: s1 - s0 }); + rep.mutate.resizeShape({ id, ds: s1 - s0 }); }; const onRotate = (e: DraggableEvent, d: DraggableData) => { @@ -60,7 +61,7 @@ export function Selection({ ); const after = Math.atan2(offsetY - shapeCenter.y, d.x - shapeCenter.x); - data.rotateShape({ id, ddeg: ((after - before) * 180) / Math.PI }); + rep.mutate.rotateShape({ id, ddeg: ((after - before) * 180) / Math.PI }); }; if (!coords) { @@ -73,7 +74,7 @@ export function Selection({
(null); + const [rep, setRep] = useState | null>(null); // TODO: Think through Replicache + SSR. useEffect(() => { (async () => { - if (data) { + if (rep) { return; } const [, , docID] = location.pathname.split("/"); - const rep = new Replicache({ + const r = new Replicache({ pushURL: `/api/replicache-push?docID=${docID}`, pullURL: `/api/replicache-pull?docID=${docID}`, useMemstore: true, @@ -28,7 +26,10 @@ export default function Home() { }); const defaultUserInfo = randUserInfo(); - const d = await createData(rep, defaultUserInfo); + await r.mutate.initClientState({ + id: await r.clientID, + defaultUserInfo, + }); Pusher.logToConsole = true; var pusher = new Pusher("d9088b47d2371d532c4c", { @@ -36,14 +37,14 @@ export default function Home() { }); var channel = pusher.subscribe("default"); channel.bind("poke", function (data: unknown) { - rep.pull(); + r.pull(); }); - setData(d); + setRep(r); })(); }, []); - if (!data) { + if (!rep) { return null; } @@ -60,8 +61,8 @@ export default function Home() { background: "rgb(229,229,229)", }} > -
); } From add66bcef69c57eb2eedf920b1ae435f4f0ff02c Mon Sep 17 00:00:00 2001 From: Aaron Boodman Date: Sun, 10 Oct 2021 07:12:47 -1000 Subject: [PATCH 5/8] Use a smaller docid --- pages/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/index.tsx b/pages/index.tsx index 5d3e925..1783682 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -7,7 +7,7 @@ function Page() { export function getServerSideProps() { return { redirect: { - destination: `/d/${nanoid()}`, + destination: `/d/${nanoid(6)}`, permanent: false, }, }; From 70e9d27aca01835103d9c7809b19dfe6cb438d36 Mon Sep 17 00:00:00 2001 From: Aaron Boodman Date: Sun, 10 Oct 2021 07:17:19 -1000 Subject: [PATCH 6/8] Move initShapes to frontend --- pages/d/[id].tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pages/d/[id].tsx b/pages/d/[id].tsx index 3616607..fe567da 100644 --- a/pages/d/[id].tsx +++ b/pages/d/[id].tsx @@ -5,6 +5,7 @@ import { Nav } from "../../frontend/nav"; import Pusher from "pusher-js"; import { M, mutators } from "../../frontend/mutators"; import { randUserInfo } from "../../frontend/client-state"; +import { randomShape } from "../../frontend/shape"; export default function Home() { const [rep, setRep] = useState | null>(null); @@ -30,6 +31,12 @@ export default function Home() { id: await r.clientID, defaultUserInfo, }); + r.onSync = (syncing: boolean) => { + if (!syncing) { + r.onSync = null; + r.mutate.initShapes(new Array(5).fill(null).map(() => randomShape())); + } + }; Pusher.logToConsole = true; var pusher = new Pusher("d9088b47d2371d532c4c", { From 30d1d44f928c7e78eeacb799603e8f80ed4ccf3c Mon Sep 17 00:00:00 2001 From: Aaron Boodman Date: Sun, 10 Oct 2021 07:18:24 -1000 Subject: [PATCH 7/8] Add comment --- pages/api/echo.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/pages/api/echo.ts b/pages/api/echo.ts index 52bbd21..276a011 100644 --- a/pages/api/echo.ts +++ b/pages/api/echo.ts @@ -1,5 +1,6 @@ import type { NextApiRequest, NextApiResponse } from "next"; +// Just here to test RTT to Next.js. export default async (req: NextApiRequest, res: NextApiResponse) => { res.send("hello, world"); res.end(); From db678315a0fd95e3261b4e09eac7448831222061 Mon Sep 17 00:00:00 2001 From: Aaron Boodman Date: Mon, 11 Oct 2021 06:11:06 -1000 Subject: [PATCH 8/8] Skip eslint on Next build --- next.config.js | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 next.config.js diff --git a/next.config.js b/next.config.js new file mode 100644 index 0000000..1571804 --- /dev/null +++ b/next.config.js @@ -0,0 +1,7 @@ +module.exports = { + eslint: { + // Warning: This allows production builds to successfully complete even if + // your project has ESLint errors. + ignoreDuringBuilds: true, + }, +};