Skip to content

Commit

Permalink
Merge pull request #5 from rocicorp/diff-cookie-based
Browse files Browse the repository at this point in the history
Implement first cut of diff sync
  • Loading branch information
aboodman committed Mar 2, 2021
2 parents 12db5c7 + fc60ff1 commit 83a1634
Show file tree
Hide file tree
Showing 8 changed files with 146 additions and 48 deletions.
23 changes: 15 additions & 8 deletions backend/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,17 @@ import { must } from "./decode";
import type { ExecuteStatementFn } from "./rds";
import type { Shape } from "../shared/shape";

export async function getCookieVersion(
executor: ExecuteStatementFn
): Promise<number> {
const result = await executor("SELECT Version FROM Cookie LIMIT 1");
const version = result.records?.[0]?.[0]?.longValue;
if (version === undefined) {
throw new Error("Could not get version field");
}
return version;
}

export async function getLastMutationID(
executor: ExecuteStatementFn,
clientID: string
Expand Down Expand Up @@ -58,12 +69,8 @@ export async function putShape(
id: string,
shape: Shape
): Promise<void> {
await executor(
"INSERT INTO Shape (Id, Content) VALUES (:id, :content) " +
"ON DUPLICATE KEY UPDATE Id = :id, Content = :content",
{
id: { stringValue: id },
content: { stringValue: JSON.stringify(shape) },
}
);
await executor(`CALL PutShape(:id, :content)`, {
id: { stringValue: id },
content: { stringValue: JSON.stringify(shape) },
});
}
44 changes: 37 additions & 7 deletions backend/rds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,12 @@ import {
RDSDataClient,
RollbackTransactionCommand,
} from "@aws-sdk/client-rds-data";
import { execute } from "fp-ts/lib/State";

const region = "us-west-2";
const dbName = "replicache_sample_replidraw__dev";
const dbName =
process.env.REPLIDRAW_DB_NAME || "replicache_sample_replidraw__dev";
console.log({ dbName });
const resourceArn =
"arn:aws:rds:us-west-2:712907626835:cluster:replicache-demo-notes";
const secretArn =
Expand Down Expand Up @@ -89,12 +92,39 @@ export async function ensureDatabase() {

async function createDatabase() {
await executeStatementInDatabase(null, "CREATE DATABASE " + dbName);
await executeStatement(
"CREATE TABLE Client (Id VARCHAR(255) PRIMARY KEY NOT NULL, LastMutationID BIGINT NOT NULL)"
);
await executeStatement(
"CREATE TABLE Shape (Id VARCHAR(255) PRIMARY KEY NOT NULL, Content TEXT)"
);

await executeStatement(`CREATE TABLE Cookie (
Version BIGINT NOT NULL)`);
await executeStatement(`CREATE TABLE Client (
Id VARCHAR(255) PRIMARY KEY NOT NULL,
LastMutationID BIGINT NOT NULL)`);
await executeStatement(`CREATE TABLE Shape (
Id VARCHAR(255) PRIMARY KEY NOT NULL,
Content TEXT NOT NULL,
Version BIGINT NOT NULL)`);

await executeStatement(`INSERT INTO Cookie (Version) VALUES (0)`);

// To calculate which rows have changed for return in the pull endpoint, we
// use a very simply global version number. This is easy to understand but
// does serialize writes.
//
// There are many different strategies for calculating changed rows and the
// details are very dependent on what you are building. Contact us if you'd
// like help: https://replicache.dev/#contact.
await executeStatement(`CREATE PROCEDURE NextVersion (OUT result BIGINT)
BEGIN
UPDATE Cookie SET Version = Version + 1;
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`);
}

async function executeStatement(
Expand Down
8 changes: 4 additions & 4 deletions frontend/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ export class Data {
return useSubscribe(
this.rep,
async (tx: ReadTransaction) => {
const shapes = await tx.scanAll({ prefix: "/shape/" });
return shapes.map(([k, _]) => k.split("/")[2]);
const shapes = await tx.scanAll({ prefix: "shape-" });
return shapes.map(([k, _]) => k.split("-")[1]);
},
[]
);
Expand All @@ -66,11 +66,11 @@ export class Data {
// 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;
return ((await tx.get(`shape-${id}`)) as unknown) as Shape;
}

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

private mutatorStorage(tx: WriteTransaction): MutatorStorage {
Expand Down
1 change: 1 addition & 0 deletions frontend/designer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ type LastDrag = { x: number; y: number };

export function Designer({ data }: { data: Data }) {
const ids = data.useShapeIDs();
console.log({ ids });

// TODO: This should be stored in Replicache too, since we will be rendering
// other users' selections.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"react": "17.0.1",
"react-dom": "17.0.1",
"react-hotkeys": "^1.1.4",
"replicache": "5.2.0",
"replicache": "6.0.0-beta.0",
"replicache-react-util": "^1.1.0"
},
"devDependencies": {
Expand Down
100 changes: 79 additions & 21 deletions pages/api/replicache-pull.ts
Original file line number Diff line number Diff line change
@@ -1,51 +1,109 @@
import * as t from "io-ts";
import type { NextApiRequest, NextApiResponse } from "next";
import { ExecuteStatementCommandOutput } from "@aws-sdk/client-rds-data";
import { ExecuteStatementCommandOutput, Field } from "@aws-sdk/client-rds-data";
import { transact } from "../../backend/rds";
import { getLastMutationID } from "../../backend/data";
import { getCookieVersion, getLastMutationID } from "../../backend/data";
import { must } from "../../backend/decode";

export default async (req: NextApiRequest, res: NextApiResponse) => {
console.log(`Processing pull`, req.body);

const pull = must(pullRequest.decode(req.body));
let cookie = pull.baseStateID == "" ? 0 : parseInt(pull.baseStateID);

console.time(`Reading all Shapes...`);
let entries;
let lastMutationID = 0;

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

// 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;

const resp: PullResponse = {
lastMutationID,
stateID: String(cookie),
patch: [],
// TODO: Remove this as soon as Replicache stops requiring it.
httpRequestInfo: {
httpStatusCode: 200,
errorMessage: "",
},
};

if (entries.records) {
const resp = {
lastMutationID,
clientView: Object.fromEntries(
entries.records.map(([id, content]) => {
const s = content?.stringValue;
if (!id.stringValue) {
throw new Error(`Shape found with no id`);
}
if (!s) {
throw new Error(`No content for shape: ${id}`);
}
return ["/shape/" + id.stringValue, JSON.parse(s)];
})
),
};
console.log(`Returning ${entries.records.length} shapes...`);
res.json(resp);
for (let row of entries.records) {
const [
{ stringValue: id },
{ stringValue: content },
{ booleanValue: deleted },
] = row as [
Field.StringValueMember,
Field.StringValueMember,
Field.BooleanValueMember
];
if (deleted) {
resp.patch.push({
op: "remove",
path: `/shape-${id}`,
});
} else {
resp.patch.push({
op: "replace",
path: `/shape-${id}`,
valueString: content,
});
}
}
}

console.log(`Returning`, resp);
res.json(resp);
res.end();
};

const pullRequest = t.type({
clientID: t.string,
baseStateID: t.string,
});

const pullResponse = t.type({
stateID: t.string,
lastMutationID: t.number,
patch: t.array(
t.union([
t.type({
op: t.literal("replace"),
path: t.string,
// TODO: This will change to be arbitrary JSON
valueString: t.string,
}),
t.type({
op: t.literal("add"),
path: t.string,
valueString: t.string,
}),
t.type({
op: t.literal("remove"),
path: t.string,
}),
])
),
// unused - will go away
httpRequestInfo: t.type({
httpStatusCode: t.number,
errorMessage: t.literal(""),
}),
});
type PullResponse = t.TypeOf<typeof pullResponse>;
9 changes: 3 additions & 6 deletions pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,13 @@ export default function Home() {

const isProd = location.host.indexOf(".vercel.app") > -1;
const rep = new Replicache({
batchURL: "/api/replicache-push",
clientViewURL: "/api/replicache-pull",
diffServerURL: isProd
? "https://serve.replicache.dev/pull"
: "http://localhost:7001/pull",
diffServerAuth: isProd ? "1000000" : "sandbox",
pushURL: "/api/replicache-push",
pullURL: "/api/replicache-pull",
// TODO: Shouldn't have to manually load wasm
wasmModule: isProd ? "/replicache.wasm" : "/replicache.dev.wasm",
syncInterval: null,
useMemstore: true,
pushDelay: 1,
});
rep.sync();

Expand Down
7 changes: 6 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2635,7 +2635,12 @@ replicache-react-util@^1.1.0:
react ">=16.0 <18.0"
replicache ">=4.0.1 <6.0"

replicache@5.2.0, "replicache@>=4.0.1 <6.0":
replicache@6.0.0-beta.0:
version "6.0.0-beta.0"
resolved "https://registry.yarnpkg.com/replicache/-/replicache-6.0.0-beta.0.tgz#ff4059094b320156c16273caf3f3a131fb21ba87"
integrity sha512-9TmSJdESIJJaTPPrHTDXVYa5ixuEzj2l6PpTtwdOfcQpsWAwadPoUTP00ulsDdpw8NREihFB2IcZyMRuxzTEmQ==

"replicache@>=4.0.1 <6.0":
version "5.2.0"
resolved "https://registry.yarnpkg.com/replicache/-/replicache-5.2.0.tgz"
integrity sha512-ja+Swi4d1RlD8uUAsZr+ibQXmalQvN4IhFB9tom8w5KdpXdyeD2mGXvUlTQYdTjJuQ/2jE5zzz/djZyhWlhCSg==
Expand Down

0 comments on commit 83a1634

Please sign in to comment.