Skip to content

Commit

Permalink
save blobs in a separate object store
Browse files Browse the repository at this point in the history
  • Loading branch information
hiddenist committed Dec 31, 2023
1 parent 8444dde commit a60b13f
Show file tree
Hide file tree
Showing 2 changed files with 94 additions and 49 deletions.
121 changes: 72 additions & 49 deletions libs/drawing-engine/src/engine/CanvasHistory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ type ClearInfo = { tool: "clear" }
export type ToolInfo = LineDrawInfo | ClearInfo

interface HistoryEntry {
imageData: Blob | null
id?: IDBValidKey
blobId: IDBValidKey | null
actions: Array<ToolInfo>
}

Expand All @@ -17,31 +18,48 @@ interface HistoryOptions {
}

enum HistoryStores {
history = "history",
actions = "actions",
blobs = "blobs",
}

interface HistorySchema {
[HistoryStores.history]: HistoryEntry
[HistoryStores.actions]: HistoryEntry
[HistoryStores.blobs]: Blob
}

class HistoryDatabase extends Database<HistoryStores, HistorySchema> {
public history = this.getStore(HistoryStores.history)
static readonly name = "history"
public actions = this.getStore(HistoryStores.actions)
public blobs = this.getStore(HistoryStores.blobs)

protected static schema = {
[HistoryStores.history]: {
[HistoryStores.actions]: {
keyPath: "id",
autoIncrement: true,
fields: {
blobId: {
unique: false,
},
actions: {
unique: false,
},
},
},
}
[HistoryStores.blobs]: {
autoIncrement: true,
},
} as const

static async create() {
return new HistoryDatabase(
await Database.createDb(
"history",
(db, resolve) => {
const store = db.createObjectStore(HistoryStores.history, this.schema[HistoryStores.history])
store.transaction.oncomplete = () => {
resolve()
}
HistoryDatabase.name,
async (db, resolve) => {
await Promise.all([
Database.createObjectStoreAsync(db, HistoryStores.actions, this.schema[HistoryStores.actions]),
Database.createObjectStoreAsync(db, HistoryStores.blobs, this.schema[HistoryStores.blobs]),
])
resolve()
},
1,
),
Expand Down Expand Up @@ -75,7 +93,7 @@ export class CanvasHistory {
}

protected async restoreHistoryFromDb() {
const historyKeys = await this.db.history.getAllKeys()
const historyKeys = await this.db.actions.getAllKeys()
this.history = historyKeys.reverse()
const state = await this.getCurrentEntry()
if (state) {
Expand All @@ -101,30 +119,29 @@ export class CanvasHistory {
}

private async save(toolInfo: ToolInfo) {
const canvas = this.engine.htmlCanvas
const toolName = toolInfo.tool
const tool = toolName === "clear" ? { updatesImageData: false } : this.engine.tools[toolName]

const current = await this.getIncompleteEntry()
const current = await this.getCurrentIncompleteEntry()
current.entry.actions.push(toolInfo)

if (tool.updatesImageData && current.entry.imageData === null) {
current.entry.imageData = await new Promise<Blob>((resolve, reject) =>
canvas.toBlob((blob) => {
if (!blob) {
reject(new Error("Could not get canvas blob"))
return
}
resolve(blob)
}),
)
}

await this.db.history.put(current.key, current.entry)
await this.db.actions.put(current.key, current.entry)
return current
}

protected async getIncompleteEntry(): Promise<{ key: IDBValidKey; entry: HistoryEntry }> {
protected async saveBlob(canvas: HTMLCanvasElement) {
const blob = await new Promise<Blob>((resolve, reject) =>
canvas.toBlob((blob) => {
if (!blob) {
reject(new Error("Could not get canvas blob"))
return
}
resolve(blob)
}),
)

const blobId = await this.db.blobs.add(blob)
return blobId
}

protected async getCurrentIncompleteEntry(): Promise<{ key: IDBValidKey; entry: HistoryEntry }> {
const current = (await this.getCurrentEntry()) ?? null
if (!current) {
return this.createNewEntry()
Expand All @@ -140,16 +157,16 @@ export class CanvasHistory {
if (!key) {
return null
}
const entry = await this.db.history.get(key)
const entry = await this.db.actions.get(key)
return { key, entry }
}

protected async createNewEntry(): Promise<{ key: IDBValidKey; entry: HistoryEntry }> {
const state: HistoryEntry = {
imageData: null,
actions: [],
blobId: await this.saveBlob(this.engine.htmlCanvas).catch(() => null),
}
const key = await this.db.history.add(state)
const key = await this.db.actions.add(state)
this.appendHistoryKey(key)
return { key, entry: state }
}
Expand All @@ -164,20 +181,20 @@ export class CanvasHistory {
return
}

const state = await this.db.history.get(key)
const state = await this.db.actions.get(key)
const undone = state.actions.pop()
if (undone) {
this.redoStack.push(undone)
}

let drawEntry: HistoryEntry
if (state.actions.length === 0) {
this.db.history.delete(key)
this.db.actions.delete(key)
this.history.shift()
const nextKey = this.history[0]
drawEntry = await this.db.history.get(nextKey)
drawEntry = await this.db.actions.get(nextKey)
} else {
this.db.history.put(key, state)
this.db.actions.put(key, state)
drawEntry = state
}

Expand Down Expand Up @@ -207,35 +224,41 @@ export class CanvasHistory {
}

const first = this.history.pop()

if (first) {
this.db.history.delete(first)
this.db.actions.delete(first)
}
}

protected async drawHistoryEntry(entry: Readonly<HistoryEntry>) {
const { actions, imageData } = entry
const { actions, blobId } = entry

if (!imageData) {
this.engine._clear()
return Promise.resolve(null)
}
const { actions: filteredActions, hasClear } = CanvasHistory.getActionsSinceClear(actions)

await this.drawBlob(imageData)
await this.drawActions(CanvasHistory.getActionsSinceClear(entry.actions))
if (!hasClear && blobId) {
const blob = await this.db.blobs.get(blobId)
await this.drawBlob(blob)
}
await this.drawActions(filteredActions)

return actions[actions.length - 1]
}

protected static getActionsSinceClear(actions: Array<ToolInfo>): Array<Exclude<ToolInfo, ClearInfo>> {
protected static getActionsSinceClear(actions: Array<ToolInfo>): {
hasClear: boolean
actions: Array<Exclude<ToolInfo, ClearInfo>>
} {
const result: Array<Exclude<ToolInfo, ClearInfo>> = []
let hasClear = false
for (const action of actions) {
if (action.tool === "clear") {
hasClear = true
result.splice(0)
} else {
result.push(action)
}
}
return result
return { actions: result, hasClear }
}

protected async drawActions(actions: Array<Exclude<ToolInfo, ClearInfo>>) {
Expand Down
22 changes: 22 additions & 0 deletions libs/drawing-engine/src/engine/Database.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
interface IDBObjectStoreSchema extends IDBObjectStoreParameters {
fields?: Record<string, IDBIndexParameters>
}

export class Database<SchemaStoreNames extends string, Schema extends Record<SchemaStoreNames, any>> {
protected constructor(protected db: IDBDatabase) {}

Expand Down Expand Up @@ -52,6 +56,24 @@ export class Database<SchemaStoreNames extends string, Schema extends Record<Sch
})
}

protected static createObjectStoreAsync(db: IDBDatabase, storeName: string, schema: IDBObjectStoreSchema = {}) {
const { fields = {}, ...options } = schema
return new Promise<IDBObjectStore>((resolve, reject) => {
const store = db.createObjectStore(storeName, options)

for (const [fieldName, fieldOptions] of Object.entries(fields)) {
store.createIndex(fieldName, fieldName, fieldOptions)
}

store.transaction.oncomplete = () => {
resolve(store)
}
store.transaction.onerror = () => {
reject(new Error("Could not create object store"))
}
})
}

public getStore<StoreName extends SchemaStoreNames>(storeName: StoreName) {
return {
add: (state: Schema[StoreName]) => this.add(storeName, state),
Expand Down

0 comments on commit a60b13f

Please sign in to comment.