Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

million checkboxes ui #7

Merged
merged 2 commits into from
Nov 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
});
}
},
});
Comment on lines +99 to +122
Copy link
Contributor

Choose a reason for hiding this comment

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

do you still need this?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

nope


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",
Copy link
Contributor

Choose a reason for hiding this comment

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

shouldn't this have --typecheck-components too?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

is that in the template now? i can add it

"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