Skip to content

Commit

Permalink
Merge pull request #8 from rocicorp/refactor
Browse files Browse the repository at this point in the history
Combine ClientState and Shape tables in single Object table
  • Loading branch information
aboodman authored Mar 2, 2021
2 parents 5e9b28d + bae8537 commit 1b38c44
Show file tree
Hide file tree
Showing 10 changed files with 163 additions and 220 deletions.
74 changes: 16 additions & 58 deletions backend/data.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,5 @@
/**
* Abstract RDS (SQL) to entities (Shape, Client).
*/

import { shape } from "../shared/shape";
import { clientState } from "../shared/client-state";
import { must } from "./decode";

import type { ExecuteStatementFn } from "./rds";
import type { Shape } from "../shared/shape";
import type { ClientState } from "../shared/client-state";
import { JSONValue } from "replicache";

export async function getCookieVersion(
executor: ExecuteStatementFn
Expand Down Expand Up @@ -49,60 +40,27 @@ export async function setLastMutationID(
);
}

export async function getShape(
executor: ExecuteStatementFn,
id: string
): Promise<Shape | null> {
const { records } = await executor(
"SELECT Content FROM Shape WHERE Id = :id",
{
id: { stringValue: id },
}
);
const content = records?.[0]?.[0]?.stringValue;
if (!content) {
return null;
}
return must(shape.decode(JSON.parse(content)));
}

export async function putShape(
export async function getObject<T extends JSONValue>(
executor: ExecuteStatementFn,
id: string,
shape: Shape
): Promise<void> {
await executor(`CALL PutShape(:id, :content)`, {
id: { stringValue: id },
content: { stringValue: JSON.stringify(shape) },
key: string
): Promise<T | null> {
const { records } = await executor("SELECT V FROM Object WHERE K = :key", {
key: { stringValue: key },
});
}

export async function getClientState(
executor: ExecuteStatementFn,
id: string
): Promise<ClientState> {
const { records } = await executor(
"SELECT Content FROM ClientState WHERE Id = :id",
{
id: { stringValue: id },
}
);
const content = records?.[0]?.[0]?.stringValue;
if (!content) {
return {
overID: "",
};
const value = records?.[0]?.[0]?.stringValue;
if (!value) {
return null;
}
return must(clientState.decode(JSON.parse(content)));
return JSON.parse(value);
}

export async function putClientState(
export async function putObject<T extends JSONValue>(
executor: ExecuteStatementFn,
id: string,
clientState: ClientState
key: string,
value: JSONValue
): Promise<void> {
await executor(`CALL PutClientState(:id, :content)`, {
id: { stringValue: id },
content: { stringValue: JSON.stringify(clientState) },
await executor(`CALL PutObject(:key, :value)`, {
key: { stringValue: key },
value: { stringValue: JSON.stringify(value) },
});
}
28 changes: 6 additions & 22 deletions backend/rds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
RDSDataClient,
RollbackTransactionCommand,
} from "@aws-sdk/client-rds-data";
import { execute } from "fp-ts/lib/State";

const region = "us-west-2";
const dbName =
Expand Down Expand Up @@ -100,16 +99,9 @@ async function createDatabase() {
LastMutationID BIGINT NOT NULL,
LastModified TIMESTAMP NOT NULL
DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP)`);
await executeStatement(`CREATE TABLE ClientState (
Id VARCHAR(255) PRIMARY KEY NOT NULL,
Content TEXT NOT NULL,
Version BIGINT NOT NULL)`);
// TODO: When https://github.com/rocicorp/replicache-sdk-js/issues/275 is
// fixed, can enable this.
//FOREIGN KEY (Id) REFERENCES Client (Id),
await executeStatement(`CREATE TABLE Shape (
Id VARCHAR(255) PRIMARY KEY NOT NULL,
Content TEXT NOT NULL,
await executeStatement(`CREATE TABLE Object (
K VARCHAR(255) PRIMARY KEY NOT NULL,
V TEXT NOT NULL,
Version BIGINT NOT NULL)`);

await executeStatement(`INSERT INTO Cookie (Version) VALUES (0)`);
Expand All @@ -127,20 +119,12 @@ async function createDatabase() {
SELECT Version INTO result FROM Cookie;
END`);

await executeStatement(`CREATE PROCEDURE PutShape (IN pId VARCHAR(255), IN pContent TEXT)
BEGIN
SET @version = 0;
CALL NextVersion(@version);
INSERT INTO Shape (Id, Content, Version) VALUES (pId, pContent, @version)
ON DUPLICATE KEY UPDATE Id = pId, Content = pContent, Version = @version;
END`);

await executeStatement(`CREATE PROCEDURE PutClientState (IN pId VARCHAR(255), IN pContent TEXT)
await executeStatement(`CREATE PROCEDURE PutObject (IN pK VARCHAR(255), IN pV TEXT)
BEGIN
SET @version = 0;
CALL NextVersion(@version);
INSERT INTO ClientState (Id, Content, Version) VALUES (pId, pContent, @version)
ON DUPLICATE KEY UPDATE Id = pId, Content = pContent, Version = @version;
INSERT INTO Object (K, V, Version) VALUES (pK, pV, @version)
ON DUPLICATE KEY UPDATE V = pV, Version = @version;
END`);
}

Expand Down
77 changes: 20 additions & 57 deletions frontend/data.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
import Replicache, {
JSONObject,
ReadTransaction,
WriteTransaction,
} from "replicache";
import Replicache, { ReadTransaction, WriteTransaction } from "replicache";
import { useSubscribe } from "replicache-react-util";
import { Shape } from "../shared/shape";
import { ClientState } from "../shared/client-state";
import { getShape, Shape } from "../shared/shape";
import { getClientState } from "../shared/client-state";
import {
createShape,
CreateShapeArgs,
Expand All @@ -14,9 +10,8 @@ import {
overShape,
OverShapeArgs,
} from "../shared/mutators";
import type { MutatorStorage } from "../shared/mutators";
import type Storage from "../shared/storage";
import { newID } from "../shared/id";
import { Type } from "io-ts";

/**
* Abstracts Replicache storage (key/value pairs) to entities (Shape).
Expand All @@ -38,21 +33,21 @@ export class Data {
this.createShape = rep.register(
"createShape",
async (tx: WriteTransaction, args: CreateShapeArgs) => {
await createShape(this.mutatorStorage(tx), args);
await createShape(this.writeStorage(tx), args);
}
);

this.moveShape = rep.register(
"moveShape",
async (tx: WriteTransaction, args: MoveShapeArgs) => {
await moveShape(this.mutatorStorage(tx), args);
await moveShape(this.writeStorage(tx), args);
}
);

this.overShape = rep.register(
"overShape",
async (tx: WriteTransaction, args: OverShapeArgs) => {
await overShape(this.mutatorStorage(tx), args);
await overShape(this.writeStorage(tx), args);
}
);
}
Expand All @@ -77,62 +72,30 @@ export class Data {
return useSubscribe(
this.rep,
(tx: ReadTransaction) => {
return this.getShape(tx, id);
return getShape(this.readStorage(tx), id);
},
null
);
}

useOverShapeID(): string | null {
return useSubscribe(this.rep, async (tx: ReadTransaction) => {
return (await this.getClientState(tx, this.clientID)).overID;
return (await getClientState(this.readStorage(tx), this.clientID)).overID;
});
}

private async getShape(
tx: ReadTransaction,
id: string
): Promise<Shape | null> {
// TODO: validate returned shape - can be wrong in case app reboots with
// new code and old storage. We can decode, but then what?
// See https://github.com/rocicorp/replicache-sdk-js/issues/285.
return ((await tx.get(`shape-${id}`)) as unknown) as Shape | null;
}

private async putShape(tx: WriteTransaction, id: string, shape: Shape) {
return await tx.put(`shape-${id}`, (shape as unknown) as JSONObject);
}

private async getClientState(
tx: ReadTransaction,
id: string
): Promise<ClientState> {
return (
(((await tx.get(
`client-state-${id}`
)) as unknown) as ClientState | null) || {
overID: "",
}
);
}

private async putClientState(
tx: WriteTransaction,
id: string,
client: ClientState
) {
return await tx.put(
`client-state-${id}`,
(client as unknown) as JSONObject
);
}

private mutatorStorage(tx: WriteTransaction): MutatorStorage {
private readStorage(tx: ReadTransaction): Storage {
return {
getShape: this.getShape.bind(null, tx),
putShape: this.putShape.bind(null, tx),
getClientState: this.getClientState.bind(null, tx),
putClientState: this.putClientState.bind(null, tx),
getObject: tx.get.bind(tx),
putObject: () => {
throw new Error("Cannot write inside ReadTransaction");
},
};
}

private writeStorage(tx: WriteTransaction): Storage {
return Object.assign(this.readStorage(tx), {
putObject: tx.put.bind(tx),
});
}
}
19 changes: 10 additions & 9 deletions frontend/designer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,15 +89,16 @@ export function Designer({ data }: { data: Data }) {
// paint the highlighted object again in a special 'highlight'
// mode.
overID && (
<Rect
{...{
key: `highlight-${overID}`,
data,
id: overID,
highlight: true,
}}
/>
)}
<Rect
{...{
key: `highlight-${overID}`,
data,
id: overID,
highlight: true,
}}
/>
)
}
</svg>
</div>
</HotKeys>
Expand Down
63 changes: 28 additions & 35 deletions pages/api/replicache-pull.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,29 +11,25 @@ export default async (req: NextApiRequest, res: NextApiResponse) => {
const pull = must(pullRequest.decode(req.body));
let cookie = pull.baseStateID == "" ? 0 : parseInt(pull.baseStateID);

console.time(`Reading all Shapes...`);
let shapes, clientStates;
console.time(`Reading all objects...`);
let entries;
let lastMutationID = 0;

await transact(async (executor) => {
[shapes, clientStates, lastMutationID, cookie] = await Promise.all([
executor("SELECT * FROM Shape WHERE Version > :version", {
version: { longValue: cookie },
}),
executor("SELECT * FROM ClientState WHERE Version > :version", {
[entries, lastMutationID, cookie] = await Promise.all([
executor("SELECT * FROM Object WHERE Version > :version", {
version: { longValue: cookie },
}),
getLastMutationID(executor, pull.clientID),
getCookieVersion(executor),
]);
});
console.log({ lastMutationID });
console.timeEnd(`Reading all Shapes...`);
console.timeEnd(`Reading all objects...`);

// Grump. Typescript seems to not understand that the argument to transact()
// is guaranteed to have been called before transact() exits.
shapes = (shapes as any) as ExecuteStatementCommandOutput;
clientStates = (clientStates as any) as ExecuteStatementCommandOutput;
entries = (entries as any) as ExecuteStatementCommandOutput;

const resp: PullResponse = {
lastMutationID,
Expand All @@ -46,31 +42,28 @@ export default async (req: NextApiRequest, res: NextApiResponse) => {
},
};

for (let entries of [shapes, clientStates]) {
if (entries.records) {
for (let row of entries.records) {
const [
{ stringValue: id },
{ stringValue: content },
{ booleanValue: deleted },
] = row as [
Field.StringValueMember,
Field.StringValueMember,
Field.BooleanValueMember
];
const prefix = entries == shapes ? "shape" : "client-state";
if (deleted) {
resp.patch.push({
op: "remove",
path: `/${prefix}-${id}`,
});
} else {
resp.patch.push({
op: "replace",
path: `/${prefix}-${id}`,
valueString: content,
});
}
if (entries.records) {
for (let row of entries.records) {
const [
{ stringValue: key },
{ stringValue: content },
{ booleanValue: deleted },
] = row as [
Field.StringValueMember,
Field.StringValueMember,
Field.BooleanValueMember
];
if (deleted) {
resp.patch.push({
op: "remove",
path: `/${key}`,
});
} else {
resp.patch.push({
op: "replace",
path: `/${key}`,
valueString: content,
});
}
}
}
Expand Down
Loading

1 comment on commit 1b38c44

@vercel
Copy link

@vercel vercel bot commented on 1b38c44 Mar 2, 2021

Choose a reason for hiding this comment

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

Please sign in to comment.