Skip to content

Commit

Permalink
Merge pull request #7 from get-convex/lee/ui-example
Browse files Browse the repository at this point in the history
million checkboxes ui
  • Loading branch information
ldanilek authored Nov 5, 2024
2 parents 927521a + f95feb2 commit 83dfbc7
Show file tree
Hide file tree
Showing 9 changed files with 585 additions and 2 deletions.
25 changes: 25 additions & 0 deletions example/convex/_generated/api.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
* @module
*/

import type * as checkboxes from "../checkboxes.js";
import type * as example from "../example.js";

import type {
Expand All @@ -24,6 +25,7 @@ import type {
* ```
*/
declare const fullApi: ApiFromModules<{
checkboxes: typeof checkboxes;
example: typeof example;
}>;
declare const fullApiWithMounts: typeof fullApi;
Expand Down Expand Up @@ -61,6 +63,29 @@ export declare const components: {
>;
};
};
checkboxCounter: {
public: {
add: FunctionReference<
"mutation",
"internal",
{ count: number; name: string; shards?: number },
null
>;
count: FunctionReference<"query", "internal", { name: string }, number>;
estimateCount: FunctionReference<
"query",
"internal",
{ name: string; readFromShards?: number; shards?: number },
any
>;
rebalance: FunctionReference<
"mutation",
"internal",
{ name: string; shards?: number },
any
>;
};
};
migrations: {
public: {
cancel: FunctionReference<
Expand Down
172 changes: 172 additions & 0 deletions example/convex/checkboxes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import { v } from "convex/values";
import { internalMutation, mutation, query } from "./_generated/server";
import { GenericMutationCtx } from "convex/server";
import { DataModel } from "./_generated/dataModel";
import { api, components } from "./_generated/api";
import { ShardedCounter } from "@convex-dev/sharded-counter";

export const NUM_BOXES = 1000000;
export const BOXES_PER_DOCUMENT = 4000;
export const NUM_DOCUMENTS = Math.floor(NUM_BOXES / BOXES_PER_DOCUMENT);

const checkboxCounter = new ShardedCounter(components.checkboxCounter, {
shards: { checkboxes: 100 },
}).for("checkboxes");

export const countCheckedBoxes = query({
args: {},
returns: v.number(),
handler: async (ctx) => {
return await checkboxCounter.count(ctx);
},
});

export const get = query({
args: { documentIdx: v.number() },
handler: async (ctx, { documentIdx }) => {
if (documentIdx < 0 || documentIdx >= NUM_DOCUMENTS) {
throw new Error("documentIdx out of range");
}
return (
await ctx.db
.query("checkboxes")
.withIndex("idx", (q) => q.eq("idx", documentIdx))
.order("asc")
.first()
)?.boxes;
},
});

const toggleHandler = async (
ctx: GenericMutationCtx<DataModel>,
{
documentIdx,
arrayIdx,
checked,
}: {
documentIdx: number;
arrayIdx: number;
checked: boolean;
}
) => {
if (documentIdx < 0 || documentIdx >= NUM_DOCUMENTS) {
throw new Error("documentIdx out of range");
}
if (arrayIdx < 0 || arrayIdx >= BOXES_PER_DOCUMENT) {
throw new Error("arrayIdx out of range");
}
let checkbox = await ctx.db
.query("checkboxes")
.withIndex("idx", (q) => q.eq("idx", documentIdx))
.first();

if (!checkbox) {
await ctx.db.insert("checkboxes", {
idx: documentIdx,
boxes: new Uint8Array(BOXES_PER_DOCUMENT / 8).buffer,
});
checkbox = await ctx.db
.query("checkboxes")
.withIndex("idx", (q) => q.eq("idx", documentIdx))
.first();
}
if (!checkbox) {
throw new Error("Failed to create checkbox");
}

const bytes = checkbox.boxes;
const view = new Uint8Array(bytes);
const newBytes = shiftBit(view, arrayIdx, checked)?.buffer;

if (newBytes) {
await ctx.db.patch(checkbox._id, {
idx: checkbox.idx,
boxes: newBytes,
});
if (checked) {
await checkboxCounter.inc(ctx);
} else {
await checkboxCounter.dec(ctx);
}
}
};

export const toggle = mutation({
args: { documentIdx: v.number(), arrayIdx: v.number(), checked: v.boolean() },
handler: toggleHandler,
});

export const seed = internalMutation({
args: {},
handler: async (ctx) => {
const boxes = await ctx.db
.query("checkboxes")
.withIndex("idx")
.order("asc")
.collect();
// Clear the table.
for (const box of boxes) {
await ctx.db.delete(box._id);
}

const bytes = new Uint8Array(BOXES_PER_DOCUMENT / 8);

// Reset the table.
for (let i = 0; i < NUM_DOCUMENTS; i++) {
await ctx.db.insert("checkboxes", {
idx: i,
boxes: bytes.buffer,
});
}
},
});

export const toggleRandom = internalMutation({
args: {},
handler: async (ctx) => {
for (let i = 0; i < 10; i++) {
const documentIdx = Math.floor(Math.random() * NUM_DOCUMENTS);
const arrayIdx = Math.floor(Math.random() * 2);
const box = await ctx.db
.query("checkboxes")
.withIndex("idx", (q) => q.eq("idx", documentIdx))
.first();
if (box) {
const jitter = Math.random() * 100000;
ctx.scheduler.runAfter(jitter, api.checkboxes.toggle, {
documentIdx,
arrayIdx,
checked: !isChecked(new Uint8Array(box.boxes), arrayIdx),
});
}
}
},
});

export const isChecked = (view: Uint8Array, arrayIdx: number) => {
const bit = arrayIdx % 8;
const uintIdx = Math.floor(arrayIdx / 8);
const byte = view ? view[uintIdx] : 0;
const shiftedBit = 1 << bit;
return !!(shiftedBit & byte);
};

export const shiftBit = (
view: Uint8Array,
arrayIdx: number,
checked: boolean
) => {
const bit = arrayIdx % 8;
const uintIdx = Math.floor(arrayIdx / 8);
const byte = view[uintIdx];
const shiftedBit = 1 << bit;
const isCurrentlyChecked = isChecked(view, arrayIdx);

// If the bit is already in the correct state, do nothing to avoid OCC.
if (isCurrentlyChecked === checked) {
return;
}

view[uintIdx] = shiftedBit ^ byte;
return view;
};
1 change: 1 addition & 0 deletions example/convex/convex.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import migrations from "@convex-dev/migrations/convex.config";

const app = defineApp();
app.use(shardedCounter);
app.use(shardedCounter, { name: "checkboxCounter" });
app.use(migrations);

export default app;
4 changes: 4 additions & 0 deletions example/convex/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,9 @@ export default defineSchema(
id: v.string(),
isDone: v.boolean(),
}),
checkboxes: defineTable({
idx: v.number(),
boxes: v.bytes(),
}).index("idx", ["idx"]),
},
);
9 changes: 7 additions & 2 deletions example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
"type": "module",
"version": "0.0.0",
"scripts": {
"dev": "convex dev --live-component-sources",
"dev:backend": "convex dev --live-component-sources",
"dev:frontend": "vite",
"dev": "npm-run-all --parallel dev:backend dev:frontend",
"logs": "convex logs",
"lint": "tsc -p convex && eslint convex"
},
Expand All @@ -15,18 +16,22 @@
"convex": "^1.17.0",
"convex-helpers": "^0.1.61",
"react": "^18.3.1",
"react-dom": "^18.3.1"
"react-dom": "^18.3.1",
"react-use": "^17.5.1",
"react-window": "^1.8.10"
},
"devDependencies": {
"@eslint/eslintrc": "^3.1.0",
"@eslint/js": "^9.9.0",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@types/react-window": "^1.8.8",
"@vitejs/plugin-react": "^4.3.1",
"eslint": "^9.9.0",
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
"eslint-plugin-react-refresh": "^0.4.9",
"globals": "^15.9.0",
"npm-run-all": "^4.1.5",
"typescript": "^5.5.0",
"typescript-eslint": "^8.0.1",
"vite": "^5.4.1"
Expand Down
Loading

0 comments on commit 83dfbc7

Please sign in to comment.