@fireproof/core 0.10.3-dev → 0.10.4-dev
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/fireproof.browser.esm.js +18886 -0
- package/dist/fireproof.browser.esm.js.map +7 -0
- package/dist/fireproof.browser.js +10 -4
- package/dist/fireproof.browser.js.map +3 -3
- package/dist/fireproof.cjs.js +10 -4
- package/dist/fireproof.cjs.js.map +2 -2
- package/dist/fireproof.esm.js +10 -4
- package/dist/fireproof.esm.js.map +2 -2
- package/package.json +3 -3
- package/src/crdt-helpers.ts +89 -0
- package/src/crdt.ts +45 -0
- package/src/database.ts +61 -0
- package/src/fireproof.ts +6 -0
- package/src/loader-helpers.ts +53 -0
- package/src/loader.ts +66 -0
- package/src/store-browser.ts +78 -0
- package/src/store-fs.ts +51 -0
- package/src/store.ts +32 -0
- package/src/transaction.ts +68 -0
- package/src/types.d.ts +38 -0
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@fireproof/core",
|
3
|
-
"version": "0.10.
|
3
|
+
"version": "0.10.4-dev",
|
4
4
|
"description": "Immutable embedded distributed database for the web",
|
5
5
|
"main": "dist/fireproof.cjs.js",
|
6
6
|
"module": "dist/fireproof.esm.js",
|
@@ -13,6 +13,7 @@
|
|
13
13
|
},
|
14
14
|
"browser": "./dist/fireproof.browser.js",
|
15
15
|
"files": [
|
16
|
+
"src",
|
16
17
|
"dist/fireproof.*"
|
17
18
|
],
|
18
19
|
"type": "module",
|
@@ -68,7 +69,6 @@
|
|
68
69
|
"@ipld/dag-cbor": "^9.0.3",
|
69
70
|
"async": "^3.2.4",
|
70
71
|
"idb": "^7.1.1",
|
71
|
-
"multiformats": "^12.0.1"
|
72
|
-
"prolly-trees": "^1.0.4"
|
72
|
+
"multiformats": "^12.0.1"
|
73
73
|
}
|
74
74
|
}
|
@@ -0,0 +1,89 @@
|
|
1
|
+
import { Link } from 'multiformats'
|
2
|
+
import { create, encode, decode } from 'multiformats/block'
|
3
|
+
import { sha256 as hasher } from 'multiformats/hashes/sha2'
|
4
|
+
import * as codec from '@ipld/dag-cbor'
|
5
|
+
import { put, get, EventData } from '@alanshaw/pail/crdt'
|
6
|
+
import { EventFetcher } from '@alanshaw/pail/clock'
|
7
|
+
|
8
|
+
import { TransactionBlockstore as Blockstore, Transaction } from './transaction'
|
9
|
+
import { DocUpdate, ClockHead, BlockFetcher, AnyLink, DocValue, BulkResult } from './types'
|
10
|
+
|
11
|
+
export function makeGetBlock(blocks: BlockFetcher) {
|
12
|
+
return async (address: Link) => {
|
13
|
+
const block = await blocks.get(address)
|
14
|
+
if (!block) throw new Error(`Missing block ${address.toString()}`)
|
15
|
+
const { cid, bytes } = block
|
16
|
+
return create({ cid, bytes, hasher, codec })
|
17
|
+
}
|
18
|
+
}
|
19
|
+
|
20
|
+
export async function applyBulkUpdateToCrdt(
|
21
|
+
tblocks: Transaction,
|
22
|
+
head: ClockHead,
|
23
|
+
updates: DocUpdate[],
|
24
|
+
options?: object
|
25
|
+
): Promise<BulkResult> {
|
26
|
+
for (const update of updates) {
|
27
|
+
const link = await makeLinkForDoc(tblocks, update)
|
28
|
+
const result = await put(tblocks, head, update.key, link, options)
|
29
|
+
for (const { cid, bytes } of [...result.additions, ...result.removals, result.event]) {
|
30
|
+
tblocks.putSync(cid, bytes)
|
31
|
+
}
|
32
|
+
head = result.head
|
33
|
+
}
|
34
|
+
return { head }
|
35
|
+
}
|
36
|
+
|
37
|
+
async function makeLinkForDoc(blocks: Transaction, update: DocUpdate): Promise<AnyLink> {
|
38
|
+
let value: DocValue
|
39
|
+
if (update.del) {
|
40
|
+
value = { del: true }
|
41
|
+
} else {
|
42
|
+
value = { doc: update.value }
|
43
|
+
}
|
44
|
+
const block = await encode({ value, hasher, codec })
|
45
|
+
blocks.putSync(block.cid, block.bytes)
|
46
|
+
return block.cid
|
47
|
+
}
|
48
|
+
|
49
|
+
export async function getValueFromCrdt(blocks: Blockstore, head: ClockHead, key: string): Promise<DocValue> {
|
50
|
+
const link = await get(blocks, head, key)
|
51
|
+
if (!link) throw new Error(`Missing key ${key}`)
|
52
|
+
return await getValueFromLink(blocks, link)
|
53
|
+
}
|
54
|
+
|
55
|
+
export async function getValueFromLink(blocks: Blockstore, link: AnyLink): Promise<DocValue> {
|
56
|
+
const block = await blocks.get(link)
|
57
|
+
if (!block) throw new Error(`Missing block ${link.toString()}`)
|
58
|
+
const { value } = (await decode({ bytes: block.bytes, hasher, codec })) as { value: DocValue }
|
59
|
+
return value
|
60
|
+
}
|
61
|
+
|
62
|
+
export async function clockChangesSince(
|
63
|
+
blocks: Blockstore,
|
64
|
+
_head: ClockHead,
|
65
|
+
_since: ClockHead
|
66
|
+
): Promise<{ result: DocUpdate[] }> {
|
67
|
+
const eventsFetcher = new EventFetcher<EventData>(blocks)
|
68
|
+
const updates = await gatherUpdates(blocks, eventsFetcher, _head, _since)
|
69
|
+
return { result: updates.reverse() }
|
70
|
+
}
|
71
|
+
|
72
|
+
async function gatherUpdates(blocks: Blockstore, eventsFetcher: EventFetcher<EventData>, head: ClockHead, since: ClockHead, updates: DocUpdate[] = []): Promise<DocUpdate[]> {
|
73
|
+
for (const link of since) {
|
74
|
+
if (head.includes(link)) {
|
75
|
+
throw new Error('found since in head, this is good, remove this error ' + updates.length)
|
76
|
+
return updates
|
77
|
+
}
|
78
|
+
}
|
79
|
+
for (const link of head) {
|
80
|
+
const { value: event } = await eventsFetcher.get(link)
|
81
|
+
const { key, value } = event.data
|
82
|
+
const docValue = await getValueFromLink(blocks, value)
|
83
|
+
updates.push({ key, value: docValue.doc, del: docValue.del })
|
84
|
+
if (event.parents) {
|
85
|
+
updates = await gatherUpdates(blocks, eventsFetcher, event.parents, since, updates)
|
86
|
+
}
|
87
|
+
}
|
88
|
+
return updates
|
89
|
+
}
|
package/src/crdt.ts
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
import { TransactionBlockstore as Blockstore } from './transaction'
|
2
|
+
import { DocUpdate, BulkResult, ClockHead } from './types'
|
3
|
+
import { clockChangesSince, applyBulkUpdateToCrdt, getValueFromCrdt } from './crdt-helpers'
|
4
|
+
|
5
|
+
export class CRDT {
|
6
|
+
name: string | null
|
7
|
+
ready: Promise<void>
|
8
|
+
|
9
|
+
private _blocks: Blockstore
|
10
|
+
private _head: ClockHead
|
11
|
+
|
12
|
+
constructor(name?: string, blocks?: Blockstore) {
|
13
|
+
this.name = name || null
|
14
|
+
this._blocks = blocks || new Blockstore(name)
|
15
|
+
this._head = []
|
16
|
+
this.ready = this._blocks.ready.then(({ head }: { head: ClockHead }) => {
|
17
|
+
this._head = head // todo multi head support here
|
18
|
+
})
|
19
|
+
}
|
20
|
+
|
21
|
+
async bulk(updates: DocUpdate[], options?: object): Promise<BulkResult> {
|
22
|
+
await this.ready
|
23
|
+
const tResult: BulkResult = await this._blocks.transaction(async tblocks => {
|
24
|
+
const { head } = await applyBulkUpdateToCrdt(tblocks, this._head, updates, options)
|
25
|
+
this._head = head // we want multi head support here if allowing calls to bulk in parallel
|
26
|
+
return { head }
|
27
|
+
})
|
28
|
+
return tResult
|
29
|
+
}
|
30
|
+
|
31
|
+
// async root(): Promise<any> {
|
32
|
+
// async eventsSince(since: EventLink<T>): Promise<{clockCIDs: CIDCounter, result: T[]}> {
|
33
|
+
// async getAll(rootCache: any = null): Promise<{root: any, cids: CIDCounter, clockCIDs: CIDCounter, result: T[]}> {
|
34
|
+
|
35
|
+
async get(key: string) {
|
36
|
+
await this.ready
|
37
|
+
const result = await getValueFromCrdt(this._blocks, this._head, key)
|
38
|
+
if (result.del) return null
|
39
|
+
return result
|
40
|
+
}
|
41
|
+
|
42
|
+
async changes(since: ClockHead) {
|
43
|
+
return await clockChangesSince(this._blocks, this._head, since)
|
44
|
+
}
|
45
|
+
}
|
package/src/database.ts
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
// @ts-ignore
|
2
|
+
import cargoQueue from 'async/cargoQueue'
|
3
|
+
import { CRDT } from './crdt'
|
4
|
+
import { Doc, BulkResult, DocUpdate, DbResponse, ClockHead } from './types'
|
5
|
+
|
6
|
+
export class Database {
|
7
|
+
name: string
|
8
|
+
config: object
|
9
|
+
_crdt: CRDT
|
10
|
+
_writeQueue: any
|
11
|
+
constructor(name: string, config = {}) {
|
12
|
+
this.name = name
|
13
|
+
this.config = config
|
14
|
+
this._crdt = new CRDT(name)
|
15
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call
|
16
|
+
this._writeQueue = cargoQueue(async (updates: DocUpdate[]) => {
|
17
|
+
return await this._crdt.bulk(updates)
|
18
|
+
})
|
19
|
+
}
|
20
|
+
|
21
|
+
async put(doc: Doc): Promise<DbResponse> {
|
22
|
+
const { _id, ...value } = doc
|
23
|
+
return await new Promise<DbResponse>((resolve, reject) => {
|
24
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
|
25
|
+
this._writeQueue.push({ key: _id, value }, function (err: Error | null, result?: BulkResult) {
|
26
|
+
if (err) reject(err)
|
27
|
+
resolve({ id: doc._id, clock: result?.head } as DbResponse)
|
28
|
+
})
|
29
|
+
})
|
30
|
+
}
|
31
|
+
|
32
|
+
async get(id: string): Promise<Doc> {
|
33
|
+
const got = await this._crdt.get(id).catch(e => {
|
34
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
35
|
+
e.message = `Not found: ${id} - ` + e.message
|
36
|
+
throw e
|
37
|
+
})
|
38
|
+
if (!got) throw new Error(`Not found: ${id}`)
|
39
|
+
const { doc } = got
|
40
|
+
return { _id: id, ...doc }
|
41
|
+
}
|
42
|
+
|
43
|
+
async del(id: string): Promise<DbResponse> {
|
44
|
+
return await new Promise<DbResponse>((resolve, reject) => {
|
45
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
|
46
|
+
this._writeQueue.push({ key: id, del: true }, function (err: Error | null, result?: BulkResult) {
|
47
|
+
if (err) reject(err)
|
48
|
+
resolve({ id, clock: result?.head } as DbResponse)
|
49
|
+
})
|
50
|
+
})
|
51
|
+
}
|
52
|
+
|
53
|
+
async changes(since: ClockHead): Promise<{ rows: { key: string; value: Doc }[] }> {
|
54
|
+
const { result } = await this._crdt.changes(since)
|
55
|
+
const rows = result.map(({ key, value }) => ({
|
56
|
+
key,
|
57
|
+
value: { _id: key, ...value } as Doc
|
58
|
+
}))
|
59
|
+
return { rows }
|
60
|
+
}
|
61
|
+
}
|
package/src/fireproof.ts
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
import { BlockView, CID } from 'multiformats'
|
2
|
+
import { Block, encode, decode } from 'multiformats/block'
|
3
|
+
import { sha256 as hasher } from 'multiformats/hashes/sha2'
|
4
|
+
import * as raw from 'multiformats/codecs/raw'
|
5
|
+
import * as CBW from '@ipld/car/buffer-writer'
|
6
|
+
import * as codec from '@ipld/dag-cbor'
|
7
|
+
import { CarReader } from '@ipld/car'
|
8
|
+
|
9
|
+
import { Transaction } from './transaction'
|
10
|
+
import { AnyBlock, BulkResult, ClockHead, AnyLink } from './types'
|
11
|
+
|
12
|
+
export async function makeCarFile(
|
13
|
+
t: Transaction,
|
14
|
+
{ head }: BulkResult,
|
15
|
+
cars: AnyLink[]
|
16
|
+
): Promise<BlockView<unknown, number, number, 1>> {
|
17
|
+
if (!head) throw new Error('no head')
|
18
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call
|
19
|
+
const fpCarHeaderBlock = (await encode({
|
20
|
+
value: { fp: { head, cars } },
|
21
|
+
hasher,
|
22
|
+
codec
|
23
|
+
})) as AnyBlock
|
24
|
+
await t.put(fpCarHeaderBlock.cid, fpCarHeaderBlock.bytes)
|
25
|
+
|
26
|
+
let size = 0
|
27
|
+
const headerSize = CBW.headerLength({ roots: [fpCarHeaderBlock.cid as CID<unknown, number, number, 1>] })
|
28
|
+
size += headerSize
|
29
|
+
for (const { cid, bytes } of t.entries()) {
|
30
|
+
size += CBW.blockLength({ cid, bytes } as Block<unknown, number, number, 1>)
|
31
|
+
}
|
32
|
+
const buffer = new Uint8Array(size)
|
33
|
+
const writer = CBW.createWriter(buffer, { headerSize })
|
34
|
+
|
35
|
+
writer.addRoot(fpCarHeaderBlock.cid as CID<unknown, number, number, 1>)
|
36
|
+
|
37
|
+
for (const { cid, bytes } of t.entries()) {
|
38
|
+
writer.write({ cid, bytes } as Block<unknown, number, number, 1>)
|
39
|
+
}
|
40
|
+
writer.close()
|
41
|
+
return await encode({ value: writer.bytes, hasher, codec: raw })
|
42
|
+
}
|
43
|
+
|
44
|
+
export async function parseCarFile(reader: CarReader): Promise<{ head: ClockHead; cars: AnyLink[] }> {
|
45
|
+
const roots = await reader.getRoots()
|
46
|
+
const header = await reader.get(roots[0])
|
47
|
+
if (!header) throw new Error('missing header block')
|
48
|
+
const got = await decode({ bytes: header.bytes, hasher, codec })
|
49
|
+
const {
|
50
|
+
fp: { head, cars }
|
51
|
+
} = got.value as { fp: { head: ClockHead; cars: AnyLink[] } }
|
52
|
+
return { head, cars }
|
53
|
+
}
|
package/src/loader.ts
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
import { CarReader } from '@ipld/car'
|
2
|
+
|
3
|
+
// import { CarStoreFS, HeaderStoreFS } from './store-fs'
|
4
|
+
import { CarStoreIDB as CarStore, HeaderStoreLS as HeaderStore } from './store-browser'
|
5
|
+
import { makeCarFile, parseCarFile } from './loader-helpers'
|
6
|
+
import { Transaction } from './transaction'
|
7
|
+
import { AnyBlock, AnyLink, BulkResult, ClockHead } from './types'
|
8
|
+
import { CID } from 'multiformats'
|
9
|
+
|
10
|
+
export class Loader {
|
11
|
+
name: string
|
12
|
+
headerStore: HeaderStore
|
13
|
+
carStore: CarStore
|
14
|
+
carLog: AnyLink[] = []
|
15
|
+
carsReaders: Map<string, CarReader> = new Map()
|
16
|
+
ready: Promise<{ head: ClockHead}> // todo this will be a map of headers by branch name
|
17
|
+
constructor(name: string) {
|
18
|
+
this.name = name
|
19
|
+
this.headerStore = new HeaderStore(name)
|
20
|
+
this.carStore = new CarStore(name)
|
21
|
+
// todo config with multiple branches
|
22
|
+
this.ready = this.headerStore.load('main').then(async header => {
|
23
|
+
if (!header) return { head: [] }
|
24
|
+
const car = await this.carStore.load(header.car)
|
25
|
+
return await this.ingestCarHead(header.car, car)
|
26
|
+
})
|
27
|
+
}
|
28
|
+
|
29
|
+
async commit(t: Transaction, done: BulkResult): Promise<AnyLink> {
|
30
|
+
const car = await makeCarFile(t, done, this.carLog)
|
31
|
+
await this.carStore.save(car)
|
32
|
+
this.carLog.push(car.cid)
|
33
|
+
await this.headerStore.save(car.cid)
|
34
|
+
return car.cid
|
35
|
+
}
|
36
|
+
|
37
|
+
async loadCar(cid: AnyLink): Promise<CarReader> {
|
38
|
+
if (this.carsReaders.has(cid.toString())) return this.carsReaders.get(cid.toString()) as CarReader
|
39
|
+
const car = await this.carStore.load(cid)
|
40
|
+
if (!car) throw new Error(`missing car file ${cid.toString()}`)
|
41
|
+
const reader = await CarReader.fromBytes(car.bytes)
|
42
|
+
this.carsReaders.set(cid.toString(), reader)
|
43
|
+
return reader
|
44
|
+
}
|
45
|
+
|
46
|
+
async ingestCarHead(cid: AnyLink, car: AnyBlock): Promise<{ head: ClockHead, cars: AnyLink[]}> {
|
47
|
+
const reader = await CarReader.fromBytes(car.bytes)
|
48
|
+
this.carsReaders.set(cid.toString(), reader)
|
49
|
+
const { head, cars } = await parseCarFile(reader)
|
50
|
+
await this.getMoreReaders(cars)
|
51
|
+
return { head, cars }
|
52
|
+
}
|
53
|
+
|
54
|
+
async getMoreReaders(cids: AnyLink[]) {
|
55
|
+
for (const cid of cids) {
|
56
|
+
await this.loadCar(cid)
|
57
|
+
}
|
58
|
+
}
|
59
|
+
|
60
|
+
async getBlock(cid: CID): Promise<AnyBlock | undefined> {
|
61
|
+
for (const [, reader] of [...this.carsReaders].reverse()) { // reverse is faster
|
62
|
+
const block = await reader.get(cid)
|
63
|
+
if (block) return block
|
64
|
+
}
|
65
|
+
}
|
66
|
+
}
|
@@ -0,0 +1,78 @@
|
|
1
|
+
import { openDB, IDBPDatabase } from 'idb'
|
2
|
+
|
3
|
+
import { AnyBlock, AnyLink } from './types'
|
4
|
+
import { CarStore, HeaderStore, StoredHeader } from './store'
|
5
|
+
|
6
|
+
export const FORMAT = '0.9'
|
7
|
+
|
8
|
+
export class CarStoreIDB extends CarStore {
|
9
|
+
keyId: string = 'public'
|
10
|
+
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
|
11
|
+
idb: IDBPDatabase<unknown> | null = null
|
12
|
+
name: string = 'default'
|
13
|
+
async withDB(dbWorkFun: (arg0: any) => any) {
|
14
|
+
if (!this.idb) {
|
15
|
+
const dbName = `fp.${FORMAT}.${this.keyId}.${this.name}.valet`
|
16
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call
|
17
|
+
this.idb = await openDB(dbName, 1, {
|
18
|
+
upgrade(db): void {
|
19
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
|
20
|
+
db.createObjectStore('cars')
|
21
|
+
}
|
22
|
+
})
|
23
|
+
}
|
24
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
25
|
+
return await dbWorkFun(this.idb)
|
26
|
+
}
|
27
|
+
|
28
|
+
async load(cid: AnyLink): Promise<AnyBlock> {
|
29
|
+
console.log('loading', cid.toString())
|
30
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
31
|
+
return await this.withDB(async (db: IDBPDatabase<unknown>) => {
|
32
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
|
33
|
+
const tx = db.transaction(['cars'], 'readonly')
|
34
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
|
35
|
+
const bytes = (await tx.objectStore('cars').get(cid.toString())) as Uint8Array
|
36
|
+
if (!bytes) throw new Error(`missing idb block ${cid.toString()}`)
|
37
|
+
console.log('loaded', cid.toString())
|
38
|
+
return { cid, bytes }
|
39
|
+
})
|
40
|
+
}
|
41
|
+
|
42
|
+
async save(car: AnyBlock): Promise<void> {
|
43
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
44
|
+
return await this.withDB(async (db: IDBPDatabase<unknown>) => {
|
45
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
|
46
|
+
const tx = db.transaction(['cars'], 'readwrite')
|
47
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
|
48
|
+
await tx.objectStore('cars').put(car.bytes, car.cid.toString())
|
49
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access
|
50
|
+
return await tx.done
|
51
|
+
})
|
52
|
+
}
|
53
|
+
}
|
54
|
+
|
55
|
+
export class HeaderStoreLS extends HeaderStore {
|
56
|
+
keyId: string = 'public'
|
57
|
+
name: string = 'default'
|
58
|
+
|
59
|
+
headerKey(branch: string) {
|
60
|
+
return `fp.${FORMAT}.${this.keyId}.${this.name}.${branch}`
|
61
|
+
}
|
62
|
+
|
63
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
64
|
+
async load(branch: string = 'main'): Promise<StoredHeader | null> {
|
65
|
+
try {
|
66
|
+
const bytes = localStorage.getItem(this.headerKey(branch))
|
67
|
+
return bytes ? this.parseHeader(bytes.toString()) : null
|
68
|
+
} catch (e) {}
|
69
|
+
}
|
70
|
+
|
71
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
72
|
+
async save(carCid: AnyLink, branch: string = 'main') {
|
73
|
+
try {
|
74
|
+
const headerKey = this.headerKey(branch)
|
75
|
+
return localStorage.setItem(headerKey, this.makeHeader(carCid))
|
76
|
+
} catch (e) {}
|
77
|
+
}
|
78
|
+
}
|
package/src/store-fs.ts
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
import { join, dirname } from 'node:path'
|
2
|
+
import { homedir } from 'node:os'
|
3
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises'
|
4
|
+
|
5
|
+
import { AnyBlock, AnyLink } from './types'
|
6
|
+
import { HeaderStore, CarStore, StoredHeader } from './store'
|
7
|
+
|
8
|
+
export const FORMAT = '0.9'
|
9
|
+
|
10
|
+
const encoder = new TextEncoder()
|
11
|
+
|
12
|
+
export const defaultConfig = {
|
13
|
+
dataDir: join(homedir(), '.fireproof', 'v' + FORMAT)
|
14
|
+
}
|
15
|
+
|
16
|
+
export class HeaderStoreFS extends HeaderStore {
|
17
|
+
async load(branch?: string): Promise<StoredHeader|null> {
|
18
|
+
branch = branch || 'main'
|
19
|
+
const filepath = join(defaultConfig.dataDir, this.name, branch + '.json')
|
20
|
+
const bytes = await readFile(filepath).catch((e: Error & { code: string}) => {
|
21
|
+
if (e.code === 'ENOENT') return null
|
22
|
+
throw e
|
23
|
+
})
|
24
|
+
return bytes ? this.parseHeader(bytes.toString()) : null
|
25
|
+
}
|
26
|
+
|
27
|
+
async save(carCid: AnyLink, branch?: string) {
|
28
|
+
branch = branch || 'main'
|
29
|
+
const filepath = join(defaultConfig.dataDir, this.name, branch + '.json')
|
30
|
+
const bytes = this.makeHeader(carCid)
|
31
|
+
await writePathFile(filepath, encoder.encode(bytes))
|
32
|
+
}
|
33
|
+
}
|
34
|
+
|
35
|
+
export class CarStoreFS extends CarStore {
|
36
|
+
async save(car: AnyBlock): Promise<void> {
|
37
|
+
const filepath = join(defaultConfig.dataDir, this.name, car.cid.toString() + '.car')
|
38
|
+
await writePathFile(filepath, car.bytes)
|
39
|
+
}
|
40
|
+
|
41
|
+
async load(cid: AnyLink): Promise<AnyBlock> {
|
42
|
+
const filepath = join(defaultConfig.dataDir, this.name, cid.toString() + '.car')
|
43
|
+
const bytes = await readFile(filepath)
|
44
|
+
return { cid, bytes: new Uint8Array(bytes) }
|
45
|
+
}
|
46
|
+
}
|
47
|
+
|
48
|
+
async function writePathFile(path: string, data: Uint8Array) {
|
49
|
+
await mkdir(dirname(path), { recursive: true })
|
50
|
+
return await writeFile(path, data)
|
51
|
+
}
|
package/src/store.ts
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
import { parse } from 'multiformats/link'
|
2
|
+
import { AnyLink } from './types'
|
3
|
+
|
4
|
+
export class StoredHeader {
|
5
|
+
car: AnyLink
|
6
|
+
constructor(jsonHeader: { car: string }) {
|
7
|
+
this.car = parse(jsonHeader.car)
|
8
|
+
}
|
9
|
+
}
|
10
|
+
|
11
|
+
export class HeaderStore {
|
12
|
+
name: string
|
13
|
+
constructor(name: string) {
|
14
|
+
this.name = name
|
15
|
+
}
|
16
|
+
|
17
|
+
makeHeader(car: AnyLink): string {
|
18
|
+
return JSON.stringify({ car: car.toString() })
|
19
|
+
}
|
20
|
+
|
21
|
+
parseHeader(headerData: string) {
|
22
|
+
const header = JSON.parse(headerData) as { car: string }
|
23
|
+
return new StoredHeader(header)
|
24
|
+
}
|
25
|
+
}
|
26
|
+
|
27
|
+
export class CarStore {
|
28
|
+
name: string
|
29
|
+
constructor(name: string) {
|
30
|
+
this.name = name
|
31
|
+
}
|
32
|
+
}
|
@@ -0,0 +1,68 @@
|
|
1
|
+
import { MemoryBlockstore } from '@alanshaw/pail/block'
|
2
|
+
import { BlockFetcher, AnyBlock, AnyLink, BulkResult, ClockHead } from './types'
|
3
|
+
import { Loader } from './loader'
|
4
|
+
import { CID } from 'multiformats'
|
5
|
+
|
6
|
+
/** forked from
|
7
|
+
* https://github.com/alanshaw/pail/blob/main/src/block.js
|
8
|
+
* thanks Alan
|
9
|
+
**/
|
10
|
+
|
11
|
+
export class Transaction extends MemoryBlockstore {
|
12
|
+
constructor(private parent: BlockFetcher) {
|
13
|
+
super()
|
14
|
+
this.parent = parent
|
15
|
+
}
|
16
|
+
|
17
|
+
async get(cid: AnyLink): Promise<AnyBlock | undefined> {
|
18
|
+
return this.parent.get(cid)
|
19
|
+
}
|
20
|
+
|
21
|
+
async superGet(cid: AnyLink): Promise<AnyBlock | undefined> {
|
22
|
+
return super.get(cid)
|
23
|
+
}
|
24
|
+
}
|
25
|
+
|
26
|
+
export class TransactionBlockstore implements BlockFetcher {
|
27
|
+
name: string | null = null
|
28
|
+
ready: Promise<{ head: ClockHead }> // todo this will be a map of headers by branch name
|
29
|
+
|
30
|
+
private transactions: Set<Transaction> = new Set()
|
31
|
+
private loader: Loader | null = null
|
32
|
+
|
33
|
+
constructor(name?: string, loader?: Loader) {
|
34
|
+
if (name) {
|
35
|
+
this.name = name
|
36
|
+
this.loader = loader || new Loader(name)
|
37
|
+
this.ready = this.loader.ready
|
38
|
+
} else {
|
39
|
+
this.ready = Promise.resolve({ head: [] })
|
40
|
+
}
|
41
|
+
}
|
42
|
+
|
43
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
44
|
+
async put() {
|
45
|
+
throw new Error('use a transaction to put')
|
46
|
+
}
|
47
|
+
|
48
|
+
async get(cid: AnyLink): Promise<AnyBlock | undefined> {
|
49
|
+
for (const f of this.transactions) {
|
50
|
+
const v = await f.superGet(cid)
|
51
|
+
if (v) return v
|
52
|
+
}
|
53
|
+
if (!this.loader) return
|
54
|
+
return await this.loader.getBlock(cid as CID)
|
55
|
+
}
|
56
|
+
|
57
|
+
async transaction(fn: (t: Transaction) => Promise<BulkResult>) {
|
58
|
+
const t = new Transaction(this)
|
59
|
+
this.transactions.add(t)
|
60
|
+
const done: BulkResult = await fn(t)
|
61
|
+
if (done) { return { ...done, car: await this.commit(t, done) } }
|
62
|
+
return done
|
63
|
+
}
|
64
|
+
|
65
|
+
async commit(t: Transaction, done: BulkResult): Promise<AnyLink | undefined> {
|
66
|
+
return await this.loader?.commit(t, done)
|
67
|
+
}
|
68
|
+
}
|
package/src/types.d.ts
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
import { Link } from 'multiformats'
|
2
|
+
import { EventLink } from '@alanshaw/pail/clock'
|
3
|
+
import { EventData } from '@alanshaw/pail/crdt'
|
4
|
+
|
5
|
+
export type ClockHead = EventLink<EventData>[]
|
6
|
+
|
7
|
+
export type BulkResult = {
|
8
|
+
head: ClockHead
|
9
|
+
car?: AnyLink
|
10
|
+
}
|
11
|
+
|
12
|
+
type DocBody = {
|
13
|
+
[key: string]: any
|
14
|
+
}
|
15
|
+
|
16
|
+
export type Doc = DocBody & {
|
17
|
+
_id: string
|
18
|
+
}
|
19
|
+
|
20
|
+
export type DocUpdate = {
|
21
|
+
key: string
|
22
|
+
value?: DocBody
|
23
|
+
del?: boolean
|
24
|
+
}
|
25
|
+
|
26
|
+
export type DocValue = {
|
27
|
+
doc?: DocBody
|
28
|
+
del?: boolean
|
29
|
+
}
|
30
|
+
|
31
|
+
export type AnyLink = Link<unknown, number, number, 1 | 0>
|
32
|
+
export type AnyBlock = { cid: AnyLink; bytes: Uint8Array }
|
33
|
+
export type BlockFetcher = { get: (link: AnyLink) => Promise<AnyBlock | undefined> }
|
34
|
+
|
35
|
+
export type DbResponse = {
|
36
|
+
id: string
|
37
|
+
clock: ClockHead
|
38
|
+
}
|