Skip to content

Commit 10cd51b

Browse files
authored
refactor: remove duplication in CAR blockstores (#114)
1 parent 7465dde commit 10cd51b

10 files changed

+1004
-589
lines changed

src/core/car/browser-car-blockstore.ts

Lines changed: 14 additions & 219 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,11 @@
33
* Writes blocks to an in-memory CAR structure instead of a file
44
*/
55

6-
import { CarWriter } from '@ipld/car'
7-
import type { Blockstore } from 'interface-blockstore'
8-
import type { AbortOptions, AwaitIterable } from 'interface-store'
9-
import toBuffer from 'it-to-buffer'
106
import type { CID } from 'multiformats/cid'
11-
import varint from 'varint'
7+
import { CARBlockstoreBase, type CARBlockstoreStats } from './car-blockstore-base.js'
8+
import { CARMemoryBackend } from './car-memory-backend.js'
129

13-
export interface CARBlockstoreStats {
14-
blocksWritten: number
15-
missingBlocks: Set<string>
16-
totalSize: number
17-
startTime: number
18-
finalized: boolean
19-
}
10+
export type { CARBlockstoreStats }
2011

2112
export interface CARBlockstoreOptions {
2213
rootCID: CID
@@ -25,205 +16,36 @@ export interface CARBlockstoreOptions {
2516
/**
2617
* A blockstore that writes blocks to an in-memory CAR structure
2718
* This eliminates the need for redundant storage during IPFS operations in the browser
28-
*/
29-
interface BlockOffset {
30-
blockStart: number // Where the actual block data starts (after varint + CID)
31-
blockLength: number // Length of just the block data
32-
}
33-
34-
/**
3519
*
3620
* @example
3721
* ```ts
3822
* import { CARWritingBlockstore } from './browser-car-blockstore.js'
3923
* import { CID } from 'multiformats/cid'
40-
* import varint from 'varint'
41-
24+
*
4225
* // Create with a placeholder or actual root CID
4326
* const blockstore = new CARWritingBlockstore({
4427
* rootCID: someCID,
4528
* })
46-
29+
*
4730
* await blockstore.initialize()
48-
31+
*
4932
* // Add blocks (same as Node.js version)
5033
* await blockstore.put(cid, blockData)
51-
34+
*
5235
* // Finalize when done
5336
* await blockstore.finalize()
54-
37+
*
5538
* // Get the complete CAR file
5639
* const carBytes = blockstore.getCarBytes() // Uint8Array ready for upload
5740
* ```
5841
*/
59-
export class CARWritingBlockstore implements Blockstore {
60-
private readonly rootCID: CID
61-
private readonly blockOffsets = new Map<string, BlockOffset>()
62-
private readonly stats: CARBlockstoreStats
63-
private carWriter: any = null
64-
private carChunks: Uint8Array[] = []
65-
private currentOffset = 0
66-
private finalized = false
67-
private initialized = false
42+
export class CARWritingBlockstore extends CARBlockstoreBase {
43+
private readonly memoryBackend: CARMemoryBackend
6844

6945
constructor(options: CARBlockstoreOptions) {
70-
this.rootCID = options.rootCID
71-
this.stats = {
72-
blocksWritten: 0,
73-
missingBlocks: new Set(),
74-
totalSize: 0,
75-
startTime: Date.now(),
76-
finalized: false,
77-
}
78-
}
79-
80-
async initialize(): Promise<void> {
81-
if (this.initialized) return
82-
83-
// Create CAR writer channel
84-
const { writer, out } = CarWriter.create([this.rootCID])
85-
this.carWriter = writer
86-
87-
// Collect CAR chunks as they're written
88-
;(async () => {
89-
for await (const chunk of out) {
90-
this.carChunks.push(chunk)
91-
}
92-
})().catch(() => {
93-
// Ignore errors during collection
94-
})
95-
96-
// Track header size by calculating it from the first chunk
97-
// Wait for the header to be written
98-
await this.carWriter._mutex
99-
100-
// Calculate header size from what's been written so far
101-
const headerSize = this.carChunks.reduce((sum, chunk) => sum + chunk.length, 0)
102-
this.currentOffset = headerSize
103-
104-
this.initialized = true
105-
}
106-
107-
async put(cid: CID, block: Uint8Array, _options?: AbortOptions): Promise<CID> {
108-
if (await this.has(cid)) {
109-
return cid
110-
}
111-
const cidStr = cid.toString()
112-
113-
if (this.finalized) {
114-
throw new Error('Cannot put blocks in finalized CAR blockstore')
115-
}
116-
117-
if (!this.initialized) {
118-
await this.initialize()
119-
}
120-
121-
// Calculate the varint that will be written
122-
const totalSectionLength = cid.bytes.length + block.length
123-
const varintBytes = varint.encode(totalSectionLength)
124-
const varintLength = varintBytes.length
125-
126-
const currentOffset = this.currentOffset
127-
128-
// Block data starts after the varint and CID
129-
const blockStart = currentOffset + varintLength + cid.bytes.length
130-
131-
// Store the offset information BEFORE writing
132-
this.blockOffsets.set(cidStr, {
133-
blockStart,
134-
blockLength: block.length,
135-
})
136-
137-
// Update offset for next block
138-
this.currentOffset = blockStart + block.length
139-
140-
// Write block to CAR
141-
await this.carWriter?.put({ cid, bytes: block })
142-
143-
// Update statistics
144-
this.stats.blocksWritten++
145-
this.stats.totalSize += block.length
146-
147-
return cid
148-
}
149-
150-
// biome-ignore lint/correctness/useYield: This method throws immediately and intentionally never yields
151-
async *get(_cid: CID, _options?: AbortOptions): AsyncGenerator<Uint8Array> {
152-
throw new Error('Not implemented for CAR blockstore in the browser.')
153-
}
154-
155-
async has(cid: CID, _options?: AbortOptions): Promise<boolean> {
156-
const cidStr = cid.toString()
157-
return this.blockOffsets.has(cidStr)
158-
}
159-
160-
async delete(_cid: CID, _options?: AbortOptions): Promise<void> {
161-
throw new Error('Delete operation not supported on CAR writing blockstore')
162-
}
163-
164-
async *putMany(
165-
source: AwaitIterable<{ cid: CID; bytes: Uint8Array | AwaitIterable<Uint8Array> }>,
166-
_options?: AbortOptions
167-
): AsyncGenerator<CID> {
168-
for await (const { cid, bytes } of source) {
169-
const block = bytes instanceof Uint8Array ? bytes : await toBuffer(bytes)
170-
yield await this.put(cid, block)
171-
}
172-
}
173-
174-
// biome-ignore lint/correctness/useYield: This method throws immediately and intentionally never yields
175-
async *getMany(
176-
_source: AwaitIterable<CID>,
177-
_options?: AbortOptions
178-
): AsyncGenerator<{ cid: CID; bytes: AsyncGenerator<Uint8Array> }> {
179-
throw new Error('Not implemented for CAR blockstore in the browser.')
180-
}
181-
182-
// biome-ignore lint/correctness/useYield: This method throws immediately and intentionally never yields
183-
async *deleteMany(_source: AwaitIterable<CID>, _options?: AbortOptions): AsyncGenerator<CID> {
184-
throw new Error('DeleteMany operation not supported on CAR writing blockstore')
185-
}
186-
187-
// biome-ignore lint/correctness/useYield: This method throws immediately and intentionally never yields
188-
async *getAll(_options?: AbortOptions): AsyncGenerator<{ cid: CID; bytes: AsyncGenerator<Uint8Array> }> {
189-
throw new Error('Not implemented for CAR blockstore in the browser.')
190-
}
191-
192-
/**
193-
* Finalize the CAR and return statistics
194-
*/
195-
async finalize(): Promise<CARBlockstoreStats> {
196-
if (this.finalized) {
197-
return this.stats
198-
}
199-
200-
if (!this.initialized) {
201-
throw new Error('Cannot finalize CAR blockstore without initialization')
202-
}
203-
204-
// Close the CAR writer to signal no more data
205-
if (this.carWriter != null) {
206-
await this.carWriter.close()
207-
this.carWriter = null
208-
}
209-
210-
// Wait a tick for any pending chunks to be collected
211-
await new Promise((resolve) => setTimeout(resolve, 0))
212-
213-
this.finalized = true
214-
this.stats.finalized = true
215-
216-
return this.stats
217-
}
218-
219-
/**
220-
* Get current statistics
221-
*/
222-
getStats(): CARBlockstoreStats {
223-
return {
224-
...this.stats,
225-
missingBlocks: new Set(this.stats.missingBlocks), // Return a copy
226-
}
46+
const backend = new CARMemoryBackend()
47+
super(options.rootCID, backend)
48+
this.memoryBackend = backend
22749
}
22850

22951
/**
@@ -235,33 +57,6 @@ export class CARWritingBlockstore implements Blockstore {
23557
throw new Error('Cannot get CAR bytes before finalization')
23658
}
23759

238-
// Combine all chunks into a single Uint8Array
239-
const totalLength = this.carChunks.reduce((sum, chunk) => sum + chunk.length, 0)
240-
const result = new Uint8Array(totalLength)
241-
let offset = 0
242-
for (const chunk of this.carChunks) {
243-
result.set(chunk, offset)
244-
offset += chunk.length
245-
}
246-
247-
return result
248-
}
249-
250-
/**
251-
* Clean up resources (called on errors)
252-
*/
253-
async cleanup(): Promise<void> {
254-
try {
255-
this.finalized = true
256-
257-
if (this.carWriter != null) {
258-
await this.carWriter.close()
259-
}
260-
261-
// Clear chunks to free memory
262-
this.carChunks.length = 0
263-
} catch {
264-
// Ignore cleanup errors
265-
}
60+
return this.memoryBackend.getCarBytes()
26661
}
26762
}

0 commit comments

Comments
 (0)