File size: 6,873 Bytes
eb1202c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 | import type { AsyncStorage } from "@solid-primitives/storage"
export type BlobReference = { id: string; url: string }
type Driver = {
get(key: string): Promise<string | null>
set(key: string, value: string): Promise<void>
remove(key: string): Promise<void>
putBlob(blob: Blob): Promise<string>
getBlob(id: string): Promise<Blob | null>
}
export type DraftStore = AsyncStorage & { putBlob(blob: Blob): Promise<BlobReference> }
const urls = new Map<string, string>()
function blobUrl(id: string, blob: Blob) {
const existing = urls.get(id)
if (existing) return existing
const url = URL.createObjectURL(blob)
urls.set(id, url)
return url
}
async function blobID(blob: Blob) {
const id = Array.from(new Uint8Array(await crypto.subtle.digest("SHA-256", await blob.arrayBuffer())))
.map((byte) => byte.toString(16).padStart(2, "0"))
.join("")
return id
}
export async function createBlobReference(blob: Blob): Promise<BlobReference> {
const id = await blobID(blob)
return { id, url: blobUrl(id, blob) }
}
export function createDraftStore(driver: Driver): DraftStore {
const versions = new Map<string, number>()
const putBlob = async (blob: Blob) => {
const id = await driver.putBlob(blob)
return { id, url: blobUrl(id, blob) }
}
const encode = async (value: unknown): Promise<unknown> => {
if (Array.isArray(value)) return Promise.all(value.map(encode))
if (!value || typeof value !== "object") return value
const item = value as Record<string, unknown>
if (item.type === "image" && typeof item.dataUrl === "string") {
const blob = await fetch(item.dataUrl).then((response) => response.blob())
const { dataUrl: _, ...rest } = item
return { ...rest, blob: { id: await driver.putBlob(blob) } }
}
if ("blob" in item && item.blob && typeof item.blob === "object") {
const blob = item.blob as Record<string, unknown>
if (typeof blob.id === "string" && blob.id.startsWith("data:")) {
const data = await fetch(blob.id).then((response) => response.blob())
return { ...item, blob: { id: await driver.putBlob(data) } }
}
return { ...item, blob: { id: blob.id } }
}
return Object.fromEntries(
await Promise.all(Object.entries(item).map(async ([key, entry]) => [key, await encode(entry)])),
)
}
const decode = async (value: unknown): Promise<unknown> => {
if (Array.isArray(value)) return Promise.all(value.map(decode))
if (!value || typeof value !== "object") return value
const item = value as Record<string, unknown>
if (item.blob && typeof item.blob === "object") {
const ref = item.blob as Record<string, unknown>
if (typeof ref.id === "string") {
const blob = await driver.getBlob(ref.id)
if (blob) return { ...item, blob: { id: ref.id, url: blobUrl(ref.id, blob) } }
}
}
return Object.fromEntries(
await Promise.all(Object.entries(item).map(async ([key, entry]) => [key, await decode(entry)])),
)
}
return {
getItem: async (key) => {
const value = await driver.get(key)
return value === null ? null : JSON.stringify(await decode(JSON.parse(value)))
},
setItem: async (key, value) => {
const version = (versions.get(key) ?? 0) + 1
versions.set(key, version)
const encoded = JSON.stringify(await encode(JSON.parse(value)))
if (versions.get(key) === version) await driver.set(key, encoded)
},
removeItem: async (key) => {
versions.set(key, (versions.get(key) ?? 0) + 1)
await driver.remove(key)
},
putBlob,
}
}
export function createBrowserDraftStore(): DraftStore {
const request = indexedDB.open("opencode-drafts", 1)
request.addEventListener("upgradeneeded", () => {
request.result.createObjectStore("documents")
request.result.createObjectStore("blobs")
})
const db = new Promise<IDBDatabase>((resolve, reject) => {
request.addEventListener("success", () => {
const database = request.result
const transaction = database.transaction(["documents", "blobs"], "readwrite")
const documents = transaction.objectStore("documents").getAll()
documents.addEventListener("success", () => {
const used = new Set<string>()
JSON.parse(`[${documents.result.join(",")}]`, (_key, item) => {
if (item?.blob && typeof item.blob.id === "string") used.add(item.blob.id)
return item
})
const blobs = transaction.objectStore("blobs").openKeyCursor()
blobs.addEventListener("success", () => {
const cursor = blobs.result
if (!cursor) return
if (!used.has(String(cursor.key))) cursor.delete()
cursor.continue()
})
})
transaction.addEventListener("complete", () => resolve(database))
transaction.addEventListener("abort", () => resolve(database))
})
request.addEventListener("error", () => reject(request.error))
})
const get = async (store: string, key: string) => {
const result = (await db).transaction(store).objectStore(store).get(key)
return new Promise<unknown>((resolve, reject) => {
result.addEventListener("success", () => resolve(result.result))
result.addEventListener("error", () => reject(result.error))
})
}
const write = async (store: string, key: string, value?: unknown) => {
const transaction = (await db).transaction(store, "readwrite")
if (value === undefined) transaction.objectStore(store).delete(key)
else transaction.objectStore(store).put(value, key)
return new Promise<void>((resolve, reject) => {
transaction.addEventListener("complete", () => resolve())
transaction.addEventListener("error", () => reject(transaction.error))
})
}
return createDraftStore({
get: async (key) => ((await get("documents", key)) as string | undefined) ?? null,
set: (key, value) => write("documents", key, value),
remove: (key) => write("documents", key),
putBlob: async (blob) => {
const id = await blobID(blob)
await write("blobs", id, blob)
return id
},
getBlob: async (id) => ((await get("blobs", id)) as Blob | undefined) ?? null,
})
}
export async function blobDataUrl(blob: BlobReference, mime: string) {
const data = await fetch(blob.url).then((response) => response.blob())
return new Promise<string>((resolve, reject) => {
const reader = new FileReader()
reader.addEventListener("error", () => reject(reader.error))
reader.addEventListener("load", () => {
const value = typeof reader.result === "string" ? reader.result : ""
resolve(`data:${mime};base64,${value.slice(value.indexOf(",") + 1)}`)
})
reader.readAsDataURL(data)
})
}
export function createLegacyBlobReference(dataUrl: string): BlobReference {
return { id: dataUrl, url: dataUrl }
}
|