web based infinite canvas
at main 109 lines 3.4 kB view raw
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}