+21
-2
src/lib/docstore/blob-filesystem.ts
+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
+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
+1
src/lib/docstore/blob-schema.ts
+16
-72
src/lib/docstore/blob-store.ts
+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
+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
+
}