Skip to content

Commit bbcdb12

Browse files
authored
feat: make blockstore identity-hash compatible (#297)
- Separate out typing for the blockstore interface - Implement `idstore` based on https://github.com/ipfs/go-ipfs-blockstore/blob/master/idstore.go - this leaves the original blockstore implementation unchanged but adds a composable layer applied on top to add compatibility with identity hashes (served as the same blockstore interface) - add `idstore` in construction of `repo.blocks` fix ipfs/js-ipfs#3289
1 parent a36e695 commit bbcdb12

File tree

7 files changed

+266
-65
lines changed

7 files changed

+266
-65
lines changed

package.json

+2
Original file line numberDiff line numberDiff line change
@@ -81,12 +81,14 @@
8181
"ipfs-repo-migrations": "^7.0.1",
8282
"ipfs-utils": "^6.0.0",
8383
"ipld-block": "^0.11.0",
84+
"it-filter": "^1.0.2",
8485
"it-map": "^1.0.2",
8586
"it-pushable": "^1.4.0",
8687
"just-safe-get": "^2.0.0",
8788
"just-safe-set": "^2.1.0",
8889
"merge-options": "^3.0.4",
8990
"multibase": "^4.0.1",
91+
"multihashes": "^4.0.2",
9092
"p-queue": "^6.0.0",
9193
"proper-lockfile": "^4.0.0",
9294
"sort-keys": "^4.0.0",

src/blockstore.js

+3-58
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const pushable = require('it-pushable')
1111
* @typedef {import("interface-datastore").Datastore} Datastore
1212
* @typedef {import("interface-datastore").Options} DatastoreOptions
1313
* @typedef {import("cids")} CID
14+
* @typedef {import('./types').Blockstore} Blockstore
1415
*/
1516

1617
/**
@@ -36,19 +37,14 @@ function maybeWithSharding (filestore, options) {
3637

3738
/**
3839
* @param {Datastore | ShardingDatastore} store
40+
* @returns {Blockstore}
3941
*/
4042
function createBaseStore (store) {
4143
return {
4244
open () {
4345
return store.open()
4446
},
45-
/**
46-
* Query the store
47-
*
48-
* @param {Query} query
49-
* @param {DatastoreOptions} [options]
50-
* @returns {AsyncIterable<Block|CID>}
51-
*/
47+
5248
async * query (query, options) {
5349
for await (const { key, value } of store.query(query, options)) {
5450
// TODO: we should make this a different method
@@ -61,40 +57,19 @@ function createBaseStore (store) {
6157
}
6258
},
6359

64-
/**
65-
* Get a single block by CID
66-
*
67-
* @param {CID} cid
68-
* @param {DatastoreOptions} [options]
69-
* @returns {Promise<Block>}
70-
*/
7160
async get (cid, options) {
7261
const key = cidToKey(cid)
7362
const blockData = await store.get(key, options)
7463

7564
return new Block(blockData, cid)
7665
},
7766

78-
/**
79-
* Like get, but for more
80-
*
81-
* @param {Iterable<CID> | AsyncIterable<CID>} cids
82-
* @param {DatastoreOptions} [options]
83-
* @returns {AsyncIterable<Block>}
84-
*/
8567
async * getMany (cids, options) {
8668
for await (const cid of cids) {
8769
yield this.get(cid, options)
8870
}
8971
},
9072

91-
/**
92-
* Write a single block to the store
93-
*
94-
* @param {Block} block
95-
* @param {DatastoreOptions} [options]
96-
* @returns {Promise<Block>}
97-
*/
9873
async put (block, options) {
9974
if (!Block.isBlock(block)) {
10075
throw new Error('invalid block')
@@ -110,13 +85,6 @@ function createBaseStore (store) {
11085
return block
11186
},
11287

113-
/**
114-
* Like put, but for more
115-
*
116-
* @param {AsyncIterable<Block>|Iterable<Block>} blocks
117-
* @param {DatastoreOptions} [options]
118-
* @returns {AsyncIterable<Block>}
119-
*/
12088
async * putMany (blocks, options) { // eslint-disable-line require-await
12189
// we cannot simply chain to `store.putMany` because we convert a CID into
12290
// a key based on the multihash only, so we lose the version & codec and
@@ -158,41 +126,18 @@ function createBaseStore (store) {
158126
yield * output
159127
},
160128

161-
/**
162-
* Does the store contain block with this CID?
163-
*
164-
* @param {CID} cid
165-
* @param {DatastoreOptions} [options]
166-
*/
167129
has (cid, options) {
168130
return store.has(cidToKey(cid), options)
169131
},
170132

171-
/**
172-
* Delete a block from the store
173-
*
174-
* @param {CID} cid
175-
* @param {DatastoreOptions} [options]
176-
* @returns {Promise<void>}
177-
*/
178133
delete (cid, options) {
179134
return store.delete(cidToKey(cid), options)
180135
},
181136

182-
/**
183-
* Delete a block from the store
184-
*
185-
* @param {AsyncIterable<any> | Iterable<any>} cids
186-
* @param {DatastoreOptions} [options]
187-
*/
188137
deleteMany (cids, options) {
189138
return store.deleteMany(map(cids, cid => cidToKey(cid)), options)
190139
},
191140

192-
/**
193-
* Close the store
194-
*
195-
*/
196141
close () {
197142
return store.close()
198143
}

src/config.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,9 @@ module.exports = (store) => {
139139
const value = m.value
140140
if (key) {
141141
const config = await configStore.get()
142-
_set(config, key, value)
142+
if (typeof config === 'object' && config !== null) {
143+
_set(config, key, value)
144+
}
143145
return _saveAll(config)
144146
}
145147
return _saveAll(value)

src/idstore.js

+138
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
'use strict'
2+
3+
const Block = require('ipld-block')
4+
const filter = require('it-filter')
5+
const mh = require('multihashes')
6+
const pushable = require('it-pushable')
7+
const drain = require('it-drain')
8+
const CID = require('cids')
9+
const errcode = require('err-code')
10+
11+
/**
12+
* @typedef {import("interface-datastore").Query} Query
13+
* @typedef {import("interface-datastore").Datastore} Datastore
14+
* @typedef {import("interface-datastore").Options} DatastoreOptions
15+
* @typedef {import('./types').Blockstore} Blockstore
16+
*/
17+
18+
/**
19+
*
20+
* @param {Blockstore} blockstore
21+
*/
22+
module.exports = createIdStore
23+
24+
/**
25+
* @param {Blockstore} store
26+
* @returns {Blockstore}
27+
*/
28+
function createIdStore (store) {
29+
return {
30+
open () {
31+
return store.open()
32+
},
33+
query (query, options) {
34+
return store.query(query, options)
35+
},
36+
37+
async get (cid, options) {
38+
const extracted = extractContents(cid)
39+
if (extracted.isIdentity) {
40+
return Promise.resolve(new Block(extracted.digest, cid))
41+
}
42+
return store.get(cid, options)
43+
},
44+
45+
async * getMany (cids, options) {
46+
for await (const cid of cids) {
47+
yield this.get(cid, options)
48+
}
49+
},
50+
51+
async put (block, options) {
52+
const { isIdentity } = extractContents(block.cid)
53+
if (isIdentity) {
54+
return Promise.resolve(block)
55+
}
56+
return store.put(block, options)
57+
},
58+
59+
async * putMany (blocks, options) {
60+
// in order to return all blocks. we're going to assemble a seperate iterable
61+
// return rather than return the resolves of store.putMany using the same
62+
// process used by blockstore.putMany
63+
const output = pushable()
64+
65+
// process.nextTick runs on the microtask queue, setImmediate runs on the next
66+
// event loop iteration so is slower. Use process.nextTick if it is available.
67+
const runner = process && process.nextTick ? process.nextTick : setImmediate
68+
69+
runner(async () => {
70+
try {
71+
await drain(store.putMany(async function * () {
72+
for await (const block of blocks) {
73+
if (!extractContents(block.cid).isIdentity) {
74+
yield block
75+
}
76+
// if non identity blocks successfully write, blocks are included in output
77+
output.push(block)
78+
}
79+
}()))
80+
81+
output.end()
82+
} catch (err) {
83+
output.end(err)
84+
}
85+
})
86+
87+
yield * output
88+
},
89+
90+
has (cid, options) {
91+
const { isIdentity } = extractContents(cid)
92+
if (isIdentity) {
93+
return Promise.resolve(true)
94+
}
95+
return store.has(cid, options)
96+
},
97+
98+
delete (cid, options) {
99+
const { isIdentity } = extractContents(cid)
100+
if (isIdentity) {
101+
return Promise.resolve()
102+
}
103+
return store.delete(cid, options)
104+
},
105+
106+
deleteMany (cids, options) {
107+
return store.deleteMany(filter(cids, (cid) => !extractContents(cid).isIdentity), options)
108+
},
109+
110+
close () {
111+
return store.close()
112+
}
113+
}
114+
}
115+
116+
/**
117+
* @param {CID} k
118+
* @returns {{ isIdentity: false } | { isIdentity: true, digest: Uint8Array}}
119+
*/
120+
function extractContents (k) {
121+
if (!CID.isCID(k)) {
122+
throw errcode(new Error('Not a valid cid'), 'ERR_INVALID_CID')
123+
}
124+
125+
// Pre-check by calling Prefix(), this much faster than extracting the hash.
126+
const decoded = mh.decode(k.multihash)
127+
128+
if (decoded.name !== 'identity') {
129+
return {
130+
isIdentity: false
131+
}
132+
}
133+
134+
return {
135+
isIdentity: true,
136+
digest: decoded.digest
137+
}
138+
}

src/index.js

+5-4
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const config = require('./config')
1616
const spec = require('./spec')
1717
const apiAddr = require('./api-addr')
1818
const blockstore = require('./blockstore')
19+
const idstore = require('./idstore')
1920
const defaultOptions = require('./default-options')
2021
const defaultDatastore = require('./default-datastore')
2122
const ERRORS = require('./errors')
@@ -66,8 +67,8 @@ class IpfsRepo {
6667
this.keys = backends.create('keys', pathJoin(this.path, 'keys'), this.options)
6768
this.pins = backends.create('pins', pathJoin(this.path, 'pins'), this.options)
6869
const blocksBaseStore = backends.create('blocks', pathJoin(this.path, 'blocks'), this.options)
69-
this.blocks = blockstore(blocksBaseStore, this.options.storageBackendOptions.blocks)
70-
70+
const blockStore = blockstore(blocksBaseStore, this.options.storageBackendOptions.blocks)
71+
this.blocks = idstore(blockStore)
7172
this.version = version(this.root)
7273
this.config = config(this.root)
7374
this.spec = spec(this.root)
@@ -437,7 +438,7 @@ module.exports.errors = ERRORS
437438
* @param {any} _config
438439
*/
439440
function buildConfig (_config) {
440-
_config.datastore = Object.assign({}, defaultDatastore, _get(_config, 'datastore', {}))
441+
_config.datastore = Object.assign({}, defaultDatastore, _get(_config, 'datastore'))
441442

442443
return _config
443444
}
@@ -449,7 +450,7 @@ function buildDatastoreSpec (_config) {
449450
/** @type { {type: string, mounts: Array<{mountpoint: string, type: string, prefix: string, child: {type: string, path: 'string', sync: boolean, shardFunc: string}}>}} */
450451
const spec = {
451452
...defaultDatastore.Spec,
452-
..._get(_config, 'datastore.Spec', {})
453+
..._get(_config, 'datastore.Spec')
453454
}
454455

455456
return {

src/types.d.ts

+56-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
import type { Datastore } from 'interface-datastore'
1+
import type { Datastore, Options as DatastoreOptions, Query } from 'interface-datastore'
22
import type { BigNumber } from 'bignumber.js'
33

4+
import type CID from 'cids'
5+
46
export type AwaitIterable<T> = Iterable<T> | AsyncIterable<T>
57
export type Await<T> = Promise<T> | T
68

@@ -56,3 +58,56 @@ export interface Stat {
5658
numObjects: BigNumber
5759
repoSize: BigNumber
5860
}
61+
62+
export interface Block {
63+
cid: CID
64+
data: Uint8Array
65+
}
66+
67+
export interface Blockstore {
68+
open: () => Promise<Void>
69+
/**
70+
* Query the store
71+
*/
72+
query: (query: Query, options?: DatastoreOptions) => AsyncIterable<Block|CID>
73+
74+
/**
75+
* Get a single block by CID
76+
*/
77+
get: (cid: CID, options?: DatastoreOptions) => Promise<Block>
78+
79+
/**
80+
* Like get, but for more
81+
*/
82+
getMany: (cids: AwaitIterable<CID>, options?: DatastoreOptions) => AsyncIterable<Block>
83+
84+
/**
85+
* Write a single block to the store
86+
*/
87+
put: (block: Block, options?: DatastoreOptions) => Promise<Block>
88+
89+
/**
90+
* Like put, but for more
91+
*/
92+
putMany: (blocks: AwaitIterable<Block>, options?: DatastoreOptions) => AsyncIterable<Block>
93+
94+
/**
95+
* Does the store contain block with this CID?
96+
*/
97+
has: (cid: CID, options?: DatastoreOptions) => Promise<boolean>
98+
99+
/**
100+
* Delete a block from the store
101+
*/
102+
delete: (cid: CID, options?: DatastoreOptions) => Promise<Void>
103+
104+
/**
105+
* Delete a block from the store
106+
*/
107+
deleteMany: (cids: AwaitIterable<any>, options?: DatastoreOptions) => AsyncIterable<Key>
108+
109+
/**
110+
* Close the store
111+
*/
112+
close: () => Promise<Void>
113+
}

0 commit comments

Comments
 (0)