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'
106import 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
2112export 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