offline-first, p2p synced, atproto enabled, feed reader

wip blobstore stuff -- where does the bloom filter live?

+21 -2
src/lib/docstore/blob-filesystem.ts
··· 1 - import {BlobPath} from './blob-schema' 1 + import {BlobPath, MetaPath} from './blob-schema' 2 2 3 3 export class BlobFileSystem { 4 4 #root: FileSystemDirectoryHandle ··· 25 25 return [await tempdir.getFileHandle(tempname, {create: true}), () => tempdir.removeEntry(tempname)] 26 26 } 27 27 28 + async metaFileHandle(path: MetaPath) { 29 + let metadir = await this.#root.getDirectoryHandle('meta', {create: true}) 30 + for (const dir of path.slice(0, -1)) { 31 + metadir = await metadir.getDirectoryHandle(dir, {create: true}) 32 + } 33 + 34 + return metadir.getFileHandle(path[path.length - 1], {create: true}) 35 + } 36 + 37 + async metaFile(path: MetaPath) { 38 + const handle = await this.metaFileHandle(path) 39 + return await handle.getFile() 40 + } 41 + 28 42 async blobDirectory(path: BlobPath, options?: FileSystemGetDirectoryOptions) { 29 43 const dirs = path.slice(0, -1) 30 44 ··· 47 61 } 48 62 } 49 63 50 - async blobFile(path: BlobPath, options?: {createFile?: boolean; createDir?: boolean}) { 64 + async blobFileHandle(path: BlobPath, options?: {createFile?: boolean; createDir?: boolean}) { 51 65 const dir = await this.blobDirectory(path, {create: options?.createDir}) 52 66 return dir.getFileHandle(path[path.length - 1], {create: options?.createFile}) 67 + } 68 + 69 + async blobFile(path: BlobPath, options?: {createFile?: boolean; createDir?: boolean}) { 70 + const handle = await this.blobFileHandle(path, options) 71 + return handle.getFile() 53 72 } 54 73 55 74 async removeBlob(path: BlobPath) {
+2 -14
src/lib/docstore/blob-metadb.ts
··· 1 - import {BaseFilter} from 'bloom-filters' 2 1 import {DBSchema, IDBPDatabase, openDB} from 'idb' 3 2 4 3 import {ProtocolError} from '#lib/errors' ··· 7 6 import {Digest} from './digest-stream' 8 7 9 8 interface BlobMetaSchema extends DBSchema { 10 - dbmeta: { 11 - key: 'iblt' 12 - value: { 13 - id: 'iblt' 14 - iblt: Parameters<typeof BaseFilter.fromJSON>[0] 15 - } 16 - } 17 9 blobmeta: { 18 10 key: BlobMeta['digest'] 19 11 value: BlobMeta ··· 27 19 } 28 20 29 21 export class BlobMetaDB { 30 - #database: IDBPDatabase<BlobMetaSchema> 31 - 32 22 static async open(prefix: string): Promise<BlobMetaDB> { 33 23 const dbprefix = prefix.replaceAll('/', '-') 34 24 const dbversion = 1 35 25 const database = await openDB<BlobMetaSchema>(`blobstore-${dbprefix}`, dbversion, { 36 26 upgrade(db) { 37 - // make the metastore 38 - db.createObjectStore('dbmeta') 39 - 40 - // make the blobstore 41 27 const blobstore = db.createObjectStore('blobmeta', {keyPath: 'digest'}) 42 28 blobstore.createIndex('by-refcount', 'refcount', {unique: false}) 43 29 blobstore.createIndex('by-bytesize', 'bytesize', {unique: false}) ··· 48 34 49 35 return new BlobMetaDB(database) 50 36 } 37 + 38 + #database: IDBPDatabase<BlobMetaSchema> 51 39 52 40 private constructor(database: IDBPDatabase<BlobMetaSchema>) { 53 41 this.#database = database
+1
src/lib/docstore/blob-schema.ts
··· 4 4 5 5 import {type Digest, digestSchema} from './digest-stream' 6 6 7 + export type MetaPath = string[] 7 8 export type BlobPath = string[] 8 9 export function digestToPath(hash: Digest): BlobPath { 9 10 const [_, digest] = hash.split(':')
+16 -72
src/lib/docstore/blob-store.ts
··· 1 1 import {Buffer} from 'buffer' // polyfill 2 - 3 2 import {InvertibleBloomFilter} from 'bloom-filters' 4 - import {z} from 'zod/mini' 5 3 6 4 import {sleep} from '#lib/async/sleep.js' 7 - import {jsonCodec} from '#lib/schema.js' 8 5 9 6 import {BlobFileSystem} from './blob-filesystem' 10 7 import {BlobMetaDB} from './blob-metadb' 11 8 import {digestToPath, BlobMetaUnmanaged, BlobMeta} from './blob-schema' 12 - import {type Digest, DigestStream, digestSchema} from './digest-stream' 9 + import {type Digest, DigestStream} from './digest-stream' 10 + import { BlobModification, BlobWAL } from './blob-wal' 13 11 14 12 declare global { 15 13 interface FileSystemFileHandle { ··· 19 17 20 18 export type BlobResult = BlobMeta & {file: File} 21 19 22 - export type BlobModification = z.infer<typeof blobModificationSchema> 23 - 24 - const blobModificationSchema = z.object({mod: z.enum(['add', 'del']), digest: digestSchema}) 25 - const blobModificationCodec = jsonCodec(z.array(blobModificationSchema)) 26 - 27 - export class BlobWAL { 28 - #key: string 29 - #modifications: BlobModification[] 30 - 31 - constructor(prefix: string) { 32 - this.#key = `blobmeta-wal-${prefix}` 33 - this.#modifications = [] 34 - this.reload() 35 - } 36 - 37 - reload() { 38 - const stored = localStorage.getItem(this.#key) || '[]' 39 - this.#modifications = z.decode(blobModificationCodec, stored) 40 - } 41 - 42 - save() { 43 - const update = z.encode(blobModificationCodec, this.#modifications) 44 - localStorage.setItem(this.#key, update) 45 - } 46 - 47 - pull() { 48 - const val = this.#modifications 49 - this.#modifications = [] 50 - 51 - return val 52 - } 53 - 54 - unshift(...mod: BlobModification[]) { 55 - this.#modifications.unshift(...mod) 56 - this.save() 57 - } 58 - 59 - push(...mod: BlobModification[]) { 60 - this.#modifications.push(...mod) 61 - this.save() 62 - } 63 - 64 - clear() { 65 - localStorage.removeItem(this.#key) 66 - this.#modifications.length = 0 67 - } 68 - } 69 - 70 20 export class BlobStore { 71 21 static async open(prefix: string): Promise<BlobStore> { 72 - const [db, root] = await Promise.all([BlobMetaDB.open(prefix), BlobFileSystem.open(prefix)]) 73 - const bloom = await this.openIblt(db) 74 - 75 - return new BlobStore(db, root, new BlobWAL(prefix), bloom) 76 - } 77 - 78 - static async openIblt(db: BlobMetaDB) { 79 - const tx = db.db.transaction('dbmeta', 'readwrite') 80 - const store = tx.objectStore('dbmeta') 81 - 82 - let iblt: InvertibleBloomFilter 22 + const [db, root] = await Promise.all([ 23 + BlobMetaDB.open(prefix), 24 + BlobFileSystem.open(prefix) 25 + ]) 83 26 84 - const extant = await store.get('iblt') 85 - if (extant) { 86 - iblt = InvertibleBloomFilter.fromJSON(extant.iblt) as InvertibleBloomFilter 27 + let bloom: InvertibleBloomFilter 28 + const bloomFile = await root.metaFile(["iblt.json"]) 29 + if (bloomFile.size) { 30 + const text = await bloomFile.text() 31 + const json = JSON.parse(text) 32 + bloom = InvertibleBloomFilter.fromJSON(json) 87 33 } else { 88 - iblt = new InvertibleBloomFilter(100, 5) 89 - await store.put({id: 'iblt', iblt: iblt.saveAsJSON() as JSON}) 34 + bloom = new InvertibleBloomFilter(200, 5) 90 35 } 91 36 92 - await tx.done 93 - return iblt 37 + return new BlobStore(db, root, new BlobWAL(prefix), bloom) 94 38 } 95 39 96 40 #abort = new AbortController() ··· 144 88 case 'add': 145 89 this.#iblt.add(hash) 146 90 break 147 - case 'del': 91 + case 'remove': 148 92 this.#iblt.remove(hash) 149 93 break 150 94 } ··· 214 158 // if blob fails, orphan file to be found during vacuum 215 159 216 160 await this.#db.db.delete('blobmeta', digest) 217 - this.#wal.push({mod: 'del', digest}) 161 + this.#wal.push({mod: 'remove', digest}) 218 162 219 163 await this.#root.removeBlob(path) 220 164 }
+51
src/lib/docstore/blob-wal.ts
··· 1 + import { z } from "zod/mini" 2 + import { jsonCodec } from "#lib/schema" 3 + import { digestSchema } from "./digest-stream" 4 + 5 + export type BlobModification = z.infer<typeof blobModificationSchema> 6 + 7 + const blobModificationSchema = z.object({mod: z.enum(['add', 'remove']), digest: digestSchema}) 8 + const blobModificationCodec = jsonCodec(z.array(blobModificationSchema)) 9 + 10 + export class BlobWAL { 11 + #key: string 12 + #modifications: BlobModification[] 13 + 14 + constructor(prefix: string) { 15 + this.#key = `blobmeta-wal-${prefix}` 16 + this.#modifications = [] 17 + this.reload() 18 + } 19 + 20 + reload() { 21 + const stored = localStorage.getItem(this.#key) || '[]' 22 + this.#modifications = z.decode(blobModificationCodec, stored) 23 + } 24 + 25 + save() { 26 + const update = z.encode(blobModificationCodec, this.#modifications) 27 + localStorage.setItem(this.#key, update) 28 + } 29 + 30 + pull() { 31 + const val = this.#modifications 32 + this.#modifications = [] 33 + 34 + return val 35 + } 36 + 37 + unshift(...mod: BlobModification[]) { 38 + this.#modifications.unshift(...mod) 39 + this.save() 40 + } 41 + 42 + push(...mod: BlobModification[]) { 43 + this.#modifications.push(...mod) 44 + this.save() 45 + } 46 + 47 + clear() { 48 + localStorage.removeItem(this.#key) 49 + this.#modifications.length = 0 50 + } 51 + }