@fireproof/core 0.10.3-dev → 0.10.4-dev
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
}
|