Skip to content

Commit 7c72804

Browse files
authored
feat: utils index (#113)
1 parent 2f90356 commit 7c72804

10 files changed

Lines changed: 1017 additions & 14 deletions

File tree

packages/pack/src/writer.js

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -112,12 +112,13 @@ export class PackWriter {
112112
if (options?.notIndexContaining) {
113113
// Tee the stream for multiple writers
114114
const teedStreams = teeMultipleStreams(readable, this.indexWriters.length)
115-
for (let i = 0; i < this.indexWriters.length; i++) {
116-
// eslint-disable-next-line no-await-in-loop
117-
await this.indexWriters[i].addBlobs(
118-
streamIndexData(teedStreams[i].getReader())
115+
const indexWriters = this.indexWriters
116+
// Start processing all writers
117+
await Promise.all(
118+
teedStreams.map((stream, i) =>
119+
indexWriters[i].addBlobs(streamIndexData(stream.getReader()))
119120
)
120-
}
121+
)
121122
return
122123
}
123124

@@ -178,10 +179,14 @@ export class PackWriter {
178179
bufferedStream(),
179180
this.indexWriters.length
180181
)
181-
for (let i = 0; i < this.indexWriters.length; i++) {
182-
// eslint-disable-next-line no-await-in-loop
183-
await this.indexWriters[i].addBlobs(streams[i], { containingMultihash })
184-
}
182+
183+
// Start processing all writers
184+
const indexWriters = this.indexWriters
185+
await Promise.all(
186+
indexWriters.map((indexWriter, i) =>
187+
indexWriter.addBlobs(streams[i], { containingMultihash })
188+
)
189+
)
185190
}
186191
}
187192

packages/streamer/src/index.js

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,9 +85,10 @@ export class HashStreamer {
8585
// 2. Read data for each Pack in batches and stream results
8686
for (const [encodedLocation, blobRanges] of locationsToRead.entries()) {
8787
const location = decodeLocation(encodedLocation)
88+
const ranges = filterFullyCoveredRanges(blobRanges)
8889
for await (const { multihash, bytes } of this.packReader.stream(
8990
location,
90-
blobRanges.length > 0 ? blobRanges : undefined // Read full pack if no ranges
91+
ranges.length > 0 ? ranges : undefined // Read full pack if no ranges
9192
)) {
9293
yield {
9394
multihash,
@@ -128,6 +129,39 @@ function decodeLocation(encodedLocation) {
128129
throw new Error(`Invalid location type: ${encodedLocation}`)
129130
}
130131

132+
/**
133+
* Filters out ranges that fully cover other ranges.
134+
*
135+
* In other words, this removes any range that completely includes at least one other range.
136+
*
137+
* @param {Array<{
138+
* offset: number;
139+
* length: number;
140+
* multihash: API.MultihashDigest;
141+
* }>} ranges - Array of ranges to filter.
142+
* @returns {Array<{
143+
* offset: number;
144+
* length: number;
145+
* multihash: API.MultihashDigest;
146+
* }>} Filtered array with large covering ranges removed.
147+
*/
148+
function filterFullyCoveredRanges(ranges) {
149+
return ranges.filter((range, i) => {
150+
// Check if this range fully covers any other range (except itself)
151+
return !ranges.some((other, j) => {
152+
if (i === j) return false
153+
154+
const rangeStart = range.offset
155+
const rangeEnd = range.offset + range.length
156+
const otherStart = other.offset
157+
const otherEnd = other.offset + other.length
158+
159+
// Does current range fully cover the other range?
160+
return rangeStart <= otherStart && rangeEnd >= otherEnd
161+
})
162+
})
163+
}
164+
131165
/**
132166
* @enum {API.VerifiableBlobType}
133167
*/

packages/utils/README.md

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,93 @@ npm install @hash-stream/utils
1010

1111
## Usage
1212

13+
### `index/unixfs`
14+
15+
Supports building unixfs like indexes for content that is not at rest stored as content addressable data, but can be served likewise.
16+
17+
#### `writeUnixFsFileLinkIndex`
18+
19+
Creates an index of `FileLink` entries for a given blob using UnixFS layout and writes them to one or more `IndexWriter`s. It returns the multihash of the final chunk in the DAG (the "containing multihash").
20+
21+
```ts
22+
import {
23+
writeUnixFsFileLinkIndex,
24+
defaultSettings,
25+
} from '@hash-stream/utils/index/unixfs'
26+
import { withMaxChunkSize } from '@ipld/unixfs/file/chunker/fixed'
27+
28+
// Prepare your blob and writers
29+
const blob = new Blob(['hello world'])
30+
const indexWriters = [
31+
/* your Hashstream IndexWriter instances */
32+
]
33+
34+
const { containingMultihash } = await writeUnixFsFileLinkIndex(
35+
blob,
36+
'/file.txt',
37+
indexWriters,
38+
{
39+
notIndexContaining: false,
40+
settings: {
41+
...defaultSettings,
42+
chunker: withMaxChunkSize(1024 * 1024),
43+
},
44+
}
45+
)
46+
```
47+
48+
**Parameters:**
49+
50+
- `blob` (`BlobLike`) – The file blob to be split into UnixFS chunks.
51+
- `path` (`string`) – Virtual path to associate with the entries in the index.
52+
- `indexWriters` (`IndexWriter[]`) – Array of writers that receive streamable index entries.
53+
- `options` (optional) (`CreateUnixFsFileLikeStreamOptions`):
54+
- `notIndexContaining` (`boolean`) – If `true`, skips indexing the containing multihash in an hierarchy.
55+
- `settings` (`Partial<UnixFSEncodeSettings>`) – Optional settings passed to the UnixFS writer.
56+
57+
**Returns:** `Promise<{ containingMultihash: MultihashDigest } | undefined>`
58+
The multihash of the final block, or `undefined` if no writers were provided.
59+
60+
---
61+
62+
#### `createUnixFsFileLinkStream`
63+
64+
Creates a `ReadableStream` of `FileLink` entries that describe the byte layout and structure of the given blob encoded as UnixFS.
65+
66+
```ts
67+
import {
68+
createUnixFsFileLinkStream,
69+
defaultSettings,
70+
} from '@hash-stream/utils/index/unixfs'
71+
import { withMaxChunkSize } from '@ipld/unixfs/file/chunker/fixed'
72+
73+
const blob = new Blob(['example content'])
74+
75+
const stream = createUnixFsFileLinkStream(blob, {
76+
settings: {
77+
...defaultSettings,
78+
chunker: withMaxChunkSize(1024 * 1024),
79+
},
80+
})
81+
82+
// Example: reading the stream
83+
const reader = stream.getReader()
84+
while (true) {
85+
const { value, done } = await reader.read()
86+
if (done) break
87+
console.log(value) // FileLink
88+
}
89+
```
90+
91+
**Parameters:**
92+
93+
- `blob` (`BlobLike`) – The input blob to be chunked and streamed as UnixFS `FileLink` entries.
94+
- `options` (optional) (`CreateUnixFsFileLikeStreamOptions`) – Options to control chunking and encoding:
95+
- `settings` (`Partial<UnixFSEncodeSettings>`) – Optional settings to configure the UnixFS encoder.
96+
97+
**Returns:** `ReadableStream<FileLink>`
98+
A stream of metadata entries (`FileLink`) describing the chunks and layout of the encoded UnixFS file.
99+
13100
### `trustless-ipfs-gateway`
14101

15102
#### `streamer.asRawUint8Array`

packages/utils/package.json

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@
2626
"exports": {
2727
"./types": "./dist/src/api.js",
2828
"./trustless-ipfs-gateway": "./dist/src/trustless-ipfs-gateway/index.js",
29-
"./trustless-ipfs-gateway/types": "./dist/src/trustless-ipfs-gateway/api.js"
29+
"./trustless-ipfs-gateway/types": "./dist/src/trustless-ipfs-gateway/api.js",
30+
"./index": "./dist/src/index/index.js",
31+
"./index/unixfs": "./dist/src/index/unixfs.js",
32+
"./index/types": "./dist/src/index/api.js"
3033
},
3134
"typesVersions": {
3235
"*": {
@@ -38,6 +41,15 @@
3841
],
3942
"trustless-ipfs-gateway/types": [
4043
"dist/src/trustless-ipfs-gateway/api.d.ts"
44+
],
45+
"index": [
46+
"dist/src/index/index.d.ts"
47+
],
48+
"index/unixfs": [
49+
"dist/src/index/unixfs.d.ts"
50+
],
51+
"index/types": [
52+
"dist/src/index/api.d.ts"
4153
]
4254
}
4355
},
@@ -48,20 +60,24 @@
4860
"dist/src/**/*.d.ts.map"
4961
],
5062
"dependencies": {
63+
"@hash-stream/pack": "workspace:^",
5164
"@hash-stream/streamer": "workspace:^",
65+
"@vascosantos/unixfs": "^3.0.3",
5266
"ipfs-core-utils": "^0.18.1",
5367
"multiformats": "^13.3.2",
5468
"uint8arrays": "^5.1.0"
5569
},
5670
"devDependencies": {
5771
"@ipld/car": "^5.4.0",
72+
"@ipld/dag-pb": "^4.0.0",
5873
"@hash-stream/eslint-config": "workspace:^",
5974
"@hash-stream/index": "workspace:^",
6075
"@hash-stream/pack": "workspace:^",
6176
"@storacha/one-webcrypto": "^1.0.1",
6277
"@types/assert": "^1.5.11",
6378
"@types/mocha": "^10.0.10",
6479
"@types/node": "^22.13.10",
80+
"@web3-storage/upload-client": "^17.1.4",
6581
"assert": "^2.1.0",
6682
"c8": "^10.1.3",
6783
"hundreds": "^0.0.9",

packages/utils/src/index/api.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export {}

packages/utils/src/index/api.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { MultihashDigest } from 'multiformats'
2+
import { UnixFSEncoderSettingsOptions } from '@web3-storage/upload-client/types'
3+
4+
export interface CreateUnixFsFileLikeStreamOptions
5+
extends UnixFSEncoderSettingsOptions {
6+
notIndexContaining?: boolean
7+
}
8+
9+
export type { MultihashDigest }

packages/utils/src/index/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * as unixfs from './unixfs.js'

0 commit comments

Comments
 (0)