web based infinite canvas
1import Dexie, { type Transaction } from "dexie";
2import { PageRecord as PageOps } from "../model";
3import type { BoardMeta, Timestamp } from "./repo";
4import type { BindingRow, MetaRow, MigrationRow, PageRow, ShapeRow } from "./web";
5
6export const DB_NAME = "inkfinite";
7
8type Migration = { id: string; apply(tx: Transaction): Promise<void> };
9
10const PAGE_ORDER_META_PREFIX = "page-order:";
11const SHAPE_ORDER_META_PREFIX = "shape-order:";
12
13const pageOrderKey = (boardId: string) => `${PAGE_ORDER_META_PREFIX}${boardId}`;
14const shapeOrderKey = (boardId: string) => `${SHAPE_ORDER_META_PREFIX}${boardId}`;
15
16/**
17 * Dexie wrapper for Inkfinite persistence
18 */
19export class InkfiniteDB extends Dexie {
20 boards!: Dexie.Table<BoardMeta, string>;
21 pages!: Dexie.Table<PageRow, [string, string]>;
22 shapes!: Dexie.Table<ShapeRow, [string, string]>;
23 bindings!: Dexie.Table<BindingRow, [string, string]>;
24 meta!: Dexie.Table<MetaRow, string>;
25 migrations!: Dexie.Table<MigrationRow, string>;
26
27 constructor(name = DB_NAME) {
28 super(name);
29
30 this.version(1).stores({
31 boards: "id, name, createdAt, updatedAt",
32 pages: "[boardId+id], boardId, updatedAt",
33 shapes: "[boardId+id], boardId, type, updatedAt",
34 bindings: "[boardId+id], boardId, type, updatedAt",
35 meta: "key",
36 migrations: "id, appliedAt",
37 }).upgrade(async (tx) => {
38 await runMigrations(tx);
39 });
40 }
41}
42
43const MIGRATIONS: Migration[] = [{
44 id: "MIG-0001",
45 async apply(tx) {
46 const boards = tx.table<BoardMeta>("boards");
47 const rows = await boards.toArray();
48 const timestamp = Date.now();
49
50 for (const row of rows) {
51 const patch: Partial<BoardMeta> = {};
52 if (!row.createdAt) {
53 patch.createdAt = timestamp;
54 }
55 if (!row.updatedAt) {
56 patch.updatedAt = timestamp;
57 }
58
59 if (Object.keys(patch).length > 0) {
60 await boards.update(row.id, patch);
61 }
62 }
63 },
64}, {
65 id: "MIG-0002",
66 async apply(tx) {
67 const boards = tx.table<BoardMeta>("boards");
68 const pages = tx.table<PageRow>("pages");
69 const meta = tx.table<MetaRow>("meta");
70 const rows = await boards.toArray();
71 const timestamp = Date.now();
72
73 for (const row of rows) {
74 const pageCount = await pages.where("boardId").equals(row.id).count();
75 if (pageCount > 0) {
76 continue;
77 }
78
79 const defaultPage = PageOps.create("Page 1");
80 await pages.add({ ...defaultPage, boardId: row.id, updatedAt: timestamp });
81 await meta.put({ key: pageOrderKey(row.id), value: [defaultPage.id] });
82 await meta.put({ key: shapeOrderKey(row.id), value: { [defaultPage.id]: [...defaultPage.shapeIds] } });
83 await boards.update(row.id, { updatedAt: timestamp });
84 }
85 },
86}];
87
88/**
89 * Known migration IDs for tracking pending migrations in the inspector.
90 */
91export const KNOWN_MIGRATION_IDS = MIGRATIONS.map((m) => m.id);
92
93/**
94 * Run pending logical migrations during schema upgrades
95 */
96export async function runMigrations(tx: Transaction): Promise<void> {
97 const migrations = tx.table<MigrationRow>("migrations");
98 const applied = await migrations.toArray();
99 const appliedIds = new Set(applied.map((row) => row.id));
100
101 for (const migration of MIGRATIONS) {
102 if (appliedIds.has(migration.id)) {
103 continue;
104 }
105
106 await migration.apply(tx);
107 await migrations.put({ id: migration.id, appliedAt: Date.now() as Timestamp });
108 }
109}