···11+/**
22+ * plcbundle - Zero-dependency library for working with PLC bundle archives
33+ *
44+ * This module provides a Bun-native implementation for reading, processing,
55+ * and cloning PLC (Placeholder DID) bundle archives. It leverages Bun's native
66+ * features for optimal performance.
77+ *
88+ * @example
99+ * ```ts
1010+ * import { PLCBundle } from "@yourscope/plcbundle-bun";
1111+ *
1212+ * // Create bundle instance
1313+ * const bundle = new PLCBundle("./bundles");
1414+ *
1515+ * // Clone from remote
1616+ * await bundle.clone("https://plcbundle.atscan.net", {
1717+ * bundles: "1-100",
1818+ * threads: 8
1919+ * });
2020+ *
2121+ * // Process operations
2222+ * await bundle.processBundles(1, 10, (op, pos, num) => {
2323+ * console.log(op.did);
2424+ * });
2525+ * ```
2626+ *
2727+ * @module
2828+ */
2929+130export { PLCBundle } from './plcbundle';
231export type {
332 BundleIndex,
433 BundleMetadata,
534 Operation,
66- ProcessCallback
3535+ ProcessCallback,
3636+ ProcessStats,
3737+ ProcessOptions,
3838+ CloneOptions,
3939+ CloneStats,
740} from './types';
+284-13
src/plcbundle.ts
···1919}
20202121/**
2222- * Bundle reader and processor for plcbundle format
2222+ * Main class for reading and processing PLC bundle archives.
2323+ *
2424+ * This class provides methods for:
2525+ * - Cloning bundles from remote repositories
2626+ * - Reading and verifying local bundles
2727+ * - Streaming operations from bundles
2828+ * - Processing bundles with custom callbacks
2929+ *
3030+ * All operations use Bun's native features for optimal performance.
3131+ *
3232+ * @example Basic usage
3333+ * ```ts
3434+ * const bundle = new PLCBundle('./my-bundles');
3535+ *
3636+ * // Get repository information
3737+ * const stats = await bundle.getStats();
3838+ * console.log(`Repository has ${stats.lastBundle} bundles`);
3939+ *
4040+ * // Stream operations from a bundle
4141+ * for await (const op of bundle.streamOperations(1)) {
4242+ * console.log(op.did);
4343+ * }
4444+ * ```
4545+ *
4646+ * @example Clone from remote
4747+ * ```ts
4848+ * const bundle = new PLCBundle('./bundles');
4949+ *
5050+ * await bundle.clone('https://plcbundle.atscan.net', {
5151+ * bundles: '1-100',
5252+ * threads: 8,
5353+ * verify: true,
5454+ * onProgress: (stats) => {
5555+ * console.log(`${stats.downloadedBundles}/${stats.totalBundles}`);
5656+ * }
5757+ * });
5858+ * ```
5959+ *
6060+ * @example Process with callback
6161+ * ```ts
6262+ * await bundle.processBundles(1, 10, (op, pos, num) => {
6363+ * if (op.did.startsWith('did:plc:')) {
6464+ * console.log(`Found DID: ${op.did}`);
6565+ * }
6666+ * }, {
6767+ * threads: 4,
6868+ * onProgress: (stats) => console.log(`${stats.totalOps} ops`)
6969+ * });
7070+ * ```
2371 */
2472export class PLCBundle {
2573 private dir: string;
2674 private indexPath: string;
2775 private cachedIndex?: BundleIndex;
28767777+ /**
7878+ * Create a new PLCBundle instance.
7979+ *
8080+ * @param dir - Directory containing bundle files (default: './')
8181+ * @param indexPath - Path to the index file (default: `${dir}/plc_bundles.json`)
8282+ *
8383+ * @example
8484+ * ```ts
8585+ * // Use default directory
8686+ * const bundle1 = new PLCBundle();
8787+ *
8888+ * // Specify custom directory
8989+ * const bundle2 = new PLCBundle('./my-bundles');
9090+ *
9191+ * // Custom directory and index path
9292+ * const bundle3 = new PLCBundle('./bundles', './custom-index.json');
9393+ * ```
9494+ */
2995 constructor(dir: string = './', indexPath?: string) {
3096 this.dir = dir.endsWith('/') ? dir : `${dir}/`;
3197 this.indexPath = indexPath || `${this.dir}plc_bundles.json`;
3298 }
339934100 /**
3535- * Load and cache the bundle index
101101+ * Load the bundle index from disk.
102102+ *
103103+ * The index is cached in memory after first load. Use `refresh` parameter
104104+ * to force reloading from disk.
105105+ *
106106+ * @param refresh - If true, reload from disk even if cached (default: false)
107107+ * @returns Promise resolving to the bundle index
108108+ * @throws Error if index file cannot be read or parsed
109109+ *
110110+ * @example
111111+ * ```ts
112112+ * // Load index (uses cache if available)
113113+ * const index = await bundle.loadIndex();
114114+ *
115115+ * // Force reload from disk
116116+ * const freshIndex = await bundle.loadIndex(true);
117117+ * ```
36118 */
37119 async loadIndex(refresh = false): Promise<BundleIndex> {
38120 if (!refresh && this.cachedIndex) {
···45127 }
4612847129 /**
4848- * Save the bundle index
130130+ * Save a bundle index to disk.
131131+ *
132132+ * Writes the index as formatted JSON and updates the in-memory cache.
133133+ * The index is written atomically using Bun's file API.
134134+ *
135135+ * @param index - The bundle index to save
136136+ * @returns Promise that resolves when save is complete
137137+ *
138138+ * @example
139139+ * ```ts
140140+ * const index = await bundle.loadIndex();
141141+ * index.last_bundle = 150;
142142+ * await bundle.saveIndex(index);
143143+ * ```
49144 */
50145 async saveIndex(index: BundleIndex): Promise<void> {
51146 await Bun.write(this.indexPath, JSON.stringify(index, null, 2));
···53148 }
5414955150 /**
5656- * Get metadata for a specific bundle
151151+ * Get metadata for a specific bundle.
152152+ *
153153+ * @param bundleNum - The bundle number to retrieve metadata for
154154+ * @returns Promise resolving to bundle metadata, or undefined if not found
155155+ *
156156+ * @example
157157+ * ```ts
158158+ * const metadata = await bundle.getMetadata(42);
159159+ * if (metadata) {
160160+ * console.log(`Bundle ${metadata.bundle_number} has ${metadata.operation_count} operations`);
161161+ * }
162162+ * ```
57163 */
58164 async getMetadata(bundleNum: number): Promise<BundleMetadata | undefined> {
59165 const index = await this.loadIndex();
···61167 }
6216863169 /**
6464- * Get bundle file path
170170+ * Get the file path for a specific bundle.
171171+ *
172172+ * Bundles are named with zero-padded 6-digit numbers (e.g., `000042.jsonl.zst`).
173173+ *
174174+ * @param bundleNum - The bundle number
175175+ * @returns Full path to the bundle file
176176+ *
177177+ * @example
178178+ * ```ts
179179+ * const path = bundle.getBundlePath(42);
180180+ * // Returns: "./bundles/000042.jsonl.zst"
181181+ * ```
65182 */
66183 getBundlePath(bundleNum: number): string {
67184 return `${this.dir}${bundleNum.toString().padStart(6, '0')}.jsonl.zst`;
68185 }
6918670187 /**
7171- * Read and decompress a bundle
188188+ * Read and decompress a bundle file.
189189+ *
190190+ * Uses Bun's native zstd decompression for optimal performance.
191191+ *
192192+ * @param bundleNum - The bundle number to read
193193+ * @returns Promise resolving to the decompressed JSONL content as a string
194194+ * @throws Error if bundle file cannot be read or decompressed
195195+ *
196196+ * @example
197197+ * ```ts
198198+ * const jsonl = await bundle.readBundle(1);
199199+ * console.log(`Bundle size: ${jsonl.length} bytes`);
200200+ * ```
72201 */
73202 async readBundle(bundleNum: number): Promise<string> {
74203 const path = this.getBundlePath(bundleNum);
···78207 }
7920880209 /**
8181- * Parse operations from bundle content
210210+ * Parse operations from JSONL content.
211211+ *
212212+ * @param content - JSONL string with one operation per line
213213+ * @returns Array of parsed operations
214214+ *
215215+ * @example
216216+ * ```ts
217217+ * const jsonl = await bundle.readBundle(1);
218218+ * const operations = bundle.parseOperations(jsonl);
219219+ * console.log(`Parsed ${operations.length} operations`);
220220+ * ```
82221 */
83222 parseOperations(content: string): Operation[] {
84223 return content
···88227 }
8922890229 /**
9191- * Stream operations from a bundle (memory efficient)
230230+ * Stream operations from a bundle (memory efficient).
231231+ *
232232+ * This async generator yields operations one at a time, which is more
233233+ * memory efficient than loading all operations at once.
234234+ *
235235+ * @param bundleNum - The bundle number to stream from
236236+ * @yields Operations from the bundle
237237+ *
238238+ * @example
239239+ * ```ts
240240+ * for await (const op of bundle.streamOperations(1)) {
241241+ * console.log(op.did);
242242+ * if (someCondition) break; // Can stop early
243243+ * }
244244+ * ```
92245 */
93246 async *streamOperations(bundleNum: number): AsyncGenerator<Operation> {
94247 const content = await this.readBundle(bundleNum);
···102255 }
103256104257 /**
105105- * Process multiple bundles with a callback (supports multi-threading)
258258+ * Process multiple bundles with a callback function.
259259+ *
260260+ * Supports both single-threaded and multi-threaded processing via {@link ProcessOptions}.
261261+ * Operations are processed in chronological order.
262262+ *
263263+ * @param start - First bundle number to process (inclusive)
264264+ * @param end - Last bundle number to process (inclusive)
265265+ * @param callback - Function called for each operation
266266+ * @param options - Processing options (threads, progress callback)
267267+ * @returns Promise resolving to processing statistics
268268+ *
269269+ * @example Single-threaded
270270+ * ```ts
271271+ * await bundle.processBundles(1, 10, (op, pos, num) => {
272272+ * console.log(`Bundle ${num}, pos ${pos}: ${op.did}`);
273273+ * });
274274+ * ```
275275+ *
276276+ * @example Multi-threaded with progress
277277+ * ```ts
278278+ * await bundle.processBundles(1, 100, (op) => {
279279+ * // Process operation
280280+ * }, {
281281+ * threads: 4,
282282+ * onProgress: (stats) => {
283283+ * console.log(`Processed ${stats.totalOps} operations`);
284284+ * }
285285+ * });
286286+ * ```
106287 */
107288 async processBundles(
108289 start: number,
···168349 }
169350170351 /**
171171- * Clone bundles from a remote repository
352352+ * Clone bundles from a remote repository.
353353+ *
354354+ * Downloads bundles via HTTP, verifies hashes, and saves progress periodically.
355355+ * Supports resuming interrupted downloads - bundles that already exist and
356356+ * pass verification will be skipped.
357357+ *
358358+ * Progress is automatically saved every 5 seconds and on completion.
359359+ *
360360+ * @param remoteUrl - Base URL of the remote bundle repository
361361+ * @param options - Clone options (bundles, threads, verification, callbacks)
362362+ * @returns Promise resolving to clone statistics
363363+ * @throws Error if remote is unreachable or bundle range is invalid
364364+ *
365365+ * @example Clone all bundles
366366+ * ```ts
367367+ * await bundle.clone('https://plcbundle.atscan.net', {
368368+ * threads: 8,
369369+ * verify: true
370370+ * });
371371+ * ```
372372+ *
373373+ * @example Clone specific range with progress
374374+ * ```ts
375375+ * await bundle.clone('https://plcbundle.atscan.net', {
376376+ * bundles: '1-100',
377377+ * threads: 4,
378378+ * onProgress: (stats) => {
379379+ * const pct = (stats.downloadedBytes / stats.totalBytes * 100).toFixed(1);
380380+ * console.log(`${pct}% complete`);
381381+ * }
382382+ * });
383383+ * ```
384384+ *
385385+ * @example Resume interrupted download
386386+ * ```ts
387387+ * // First run - interrupted
388388+ * await bundle.clone('https://plcbundle.atscan.net', { bundles: '1-1000' });
389389+ *
390390+ * // Second run - automatically resumes
391391+ * await bundle.clone('https://plcbundle.atscan.net', { bundles: '1-1000' });
392392+ * ```
172393 */
173394 async clone(
174395 remoteUrl: string,
···398619 }
399620400621 /**
401401- * Verify bundle integrity
622622+ * Verify the integrity of a bundle.
623623+ *
624624+ * Checks both the compressed file hash and the content hash against
625625+ * the values stored in the bundle index.
626626+ *
627627+ * Uses Bun's native SHA-256 hasher for optimal performance.
628628+ *
629629+ * @param bundleNum - The bundle number to verify
630630+ * @returns Promise resolving to verification result
631631+ *
632632+ * @example
633633+ * ```ts
634634+ * const result = await bundle.verifyBundle(42);
635635+ * if (result.valid) {
636636+ * console.log('Bundle is valid');
637637+ * } else {
638638+ * console.error('Verification failed:');
639639+ * result.errors.forEach(err => console.error(` - ${err}`));
640640+ * }
641641+ * ```
402642 */
403643 async verifyBundle(bundleNum: number): Promise<{ valid: boolean; errors: string[] }> {
404644 const metadata = await this.getMetadata(bundleNum);
···429669 }
430670431671 /**
432432- * Calculate chain hash
672672+ * Calculate a chain hash linking bundles together.
673673+ *
674674+ * The chain hash ensures bundles form an unbroken chain and haven't
675675+ * been tampered with. Genesis bundles use a special hash format.
676676+ *
677677+ * @param parentHash - Hash of the parent bundle (empty for genesis)
678678+ * @param contentHash - Hash of this bundle's content
679679+ * @param isGenesis - Whether this is the first bundle in the chain
680680+ * @returns The calculated chain hash as a hex string
681681+ *
682682+ * @example
683683+ * ```ts
684684+ * // Genesis bundle
685685+ * const genesisHash = bundle.calculateChainHash('', contentHash, true);
686686+ *
687687+ * // Subsequent bundle
688688+ * const chainHash = bundle.calculateChainHash(parentHash, contentHash, false);
689689+ * ```
433690 */
434691 calculateChainHash(parentHash: string, contentHash: string, isGenesis: boolean): string {
435692 const input = isGenesis
···440697 }
441698442699 /**
443443- * Get bundle statistics
700700+ * Get repository statistics.
701701+ *
702702+ * Provides a quick overview of the bundle repository without loading
703703+ * all bundle metadata.
704704+ *
705705+ * @returns Promise resolving to repository statistics
706706+ *
707707+ * @example
708708+ * ```ts
709709+ * const stats = await bundle.getStats();
710710+ * console.log(`Version: ${stats.version}`);
711711+ * console.log(`Bundles: ${stats.totalBundles}`);
712712+ * console.log(`Size: ${(stats.totalSize / 1e9).toFixed(2)} GB`);
713713+ * console.log(`Updated: ${stats.updatedAt}`);
714714+ * ```
444715 */
445716 async getStats(): Promise<{
446717 version: string;
+155
src/types.ts
···11+/**
22+ * Type definitions for plcbundle library
33+ *
44+ * This module contains all TypeScript type definitions used throughout
55+ * the plcbundle library.
66+ *
77+ * @module
88+ */
99+1010+/**
1111+ * Metadata for a single bundle in the repository.
1212+ *
1313+ * Contains information about the bundle's contents, hashes for verification,
1414+ * and temporal boundaries.
1515+ */
116export interface BundleMetadata {
1717+ /** Sequential number identifying this bundle (e.g., 1, 2, 3...) */
218 bundle_number: number;
1919+2020+ /** ISO 8601 timestamp of the first operation in this bundle */
321 start_time: string;
2222+2323+ /** ISO 8601 timestamp of the last operation in this bundle */
424 end_time: string;
2525+2626+ /** Total number of PLC operations contained in this bundle */
527 operation_count: number;
2828+2929+ /** Number of unique DIDs referenced in this bundle */
630 did_count: number;
3131+3232+ /** Chain hash linking this bundle to its predecessor */
733 hash: string;
3434+3535+ /** SHA-256 hash of the uncompressed JSONL content */
836 content_hash: string;
3737+3838+ /** Chain hash of the previous bundle (empty string for genesis bundle) */
939 parent: string;
4040+4141+ /** SHA-256 hash of the compressed .jsonl.zst file */
1042 compressed_hash: string;
4343+4444+ /** Size of the compressed bundle file in bytes */
1145 compressed_size: number;
4646+4747+ /** Size of the uncompressed JSONL content in bytes */
1248 uncompressed_size: number;
4949+5050+ /** Cursor for fetching subsequent operations (end_time of previous bundle) */
1351 cursor: string;
5252+5353+ /** ISO 8601 timestamp when this bundle was created */
1454 created_at: string;
1555}
16565757+/**
5858+ * Index file containing metadata for all bundles in a repository.
5959+ *
6060+ * This is the main entry point for discovering available bundles.
6161+ * Located at `plc_bundles.json` in the repository root.
6262+ */
1763export interface BundleIndex {
6464+ /** Version of the index format (currently "1.0") */
1865 version: string;
6666+6767+ /** Bundle number of the most recent bundle */
1968 last_bundle: number;
6969+7070+ /** ISO 8601 timestamp when the index was last updated */
2071 updated_at: string;
7272+7373+ /** Total size of all compressed bundles in bytes */
2174 total_size_bytes: number;
7575+7676+ /** Array of metadata for each bundle, sorted by bundle_number */
2277 bundles: BundleMetadata[];
2378}
24798080+/**
8181+ * A single PLC operation as stored in bundles.
8282+ *
8383+ * Operations represent changes to DID documents in the PLC directory.
8484+ */
2585export interface Operation {
8686+ /** Decentralized Identifier (DID) this operation applies to */
2687 did: string;
8888+8989+ /** Content Identifier (CID) of this operation */
2790 cid: string;
9191+9292+ /** The actual operation data containing DID document changes */
2893 operation: any;
9494+9595+ /** ISO 8601 timestamp when this operation was created */
2996 createdAt: string;
9797+9898+ /** Additional fields that may be present in operations */
3099 [key: string]: any;
31100}
32101102102+/**
103103+ * Callback function called for each operation during processing.
104104+ *
105105+ * @param op - The operation being processed
106106+ * @param position - Zero-based position of the operation within its bundle
107107+ * @param bundleNum - The bundle number being processed
108108+ *
109109+ * @example
110110+ * ```ts
111111+ * const callback: ProcessCallback = (op, position, bundleNum) => {
112112+ * if (op.did.startsWith('did:plc:test')) {
113113+ * console.log(`Found test DID at bundle ${bundleNum}, position ${position}`);
114114+ * }
115115+ * };
116116+ * ```
117117+ */
33118export type ProcessCallback = (
34119 op: Operation,
35120 position: number,
36121 bundleNum: number
37122) => void | Promise<void>;
38123124124+/**
125125+ * Statistics collected during bundle processing.
126126+ *
127127+ * Tracks the number of operations and bytes processed.
128128+ */
39129export interface ProcessStats {
130130+ /** Total number of operations processed */
40131 totalOps: number;
132132+133133+ /** Number of operations that matched criteria (if applicable) */
41134 matchCount: number;
135135+136136+ /** Total bytes of operation data processed */
42137 totalBytes: number;
138138+139139+ /** Bytes of data for matched operations (if applicable) */
43140 matchedBytes: number;
44141}
45142143143+/**
144144+ * Options for processing bundles.
145145+ *
146146+ * Allows customization of parallel processing and progress reporting.
147147+ */
46148export interface ProcessOptions {
149149+ /** Number of worker threads to use for parallel processing (default: 1) */
47150 threads?: number;
151151+152152+ /**
153153+ * Callback invoked periodically to report processing progress.
154154+ * Called approximately every 10,000 operations.
155155+ *
156156+ * @param stats - Current processing statistics
157157+ */
48158 onProgress?: (stats: ProcessStats) => void;
49159}
50160161161+/**
162162+ * Options for cloning bundles from a remote repository.
163163+ *
164164+ * Controls download behavior, verification, and progress reporting.
165165+ */
51166export interface CloneOptions {
167167+ /** Number of parallel download threads (default: 4) */
52168 threads?: number;
169169+170170+ /**
171171+ * Bundle selection specification.
172172+ *
173173+ * Can be:
174174+ * - A single bundle number: `"42"`
175175+ * - A range: `"1-100"`
176176+ * - Undefined to clone all available bundles
177177+ */
53178 bundles?: string;
179179+180180+ /** Whether to verify SHA-256 hashes of downloaded bundles (default: true) */
54181 verify?: boolean;
182182+183183+ /**
184184+ * Function to check if cloning should stop (for graceful shutdown).
185185+ *
186186+ * @returns `true` if cloning should stop, `false` to continue
187187+ */
55188 shouldStop?: () => boolean;
189189+190190+ /**
191191+ * Callback invoked to report download progress.
192192+ *
193193+ * @param stats - Current download statistics
194194+ */
56195 onProgress?: (stats: CloneStats) => void;
57196}
58197198198+/**
199199+ * Statistics collected during bundle cloning.
200200+ *
201201+ * Tracks download progress, including successes, skips, and failures.
202202+ */
59203export interface CloneStats {
204204+ /** Total number of bundles to download */
60205 totalBundles: number;
206206+207207+ /** Number of bundles successfully downloaded in this session */
61208 downloadedBundles: number;
209209+210210+ /** Number of bundles skipped (already existed and verified) */
62211 skippedBundles: number;
212212+213213+ /** Number of bundles that failed to download */
63214 failedBundles: number;
215215+216216+ /** Total bytes to download across all bundles */
64217 totalBytes: number;
218218+219219+ /** Bytes downloaded so far (including skipped bundles) */
65220 downloadedBytes: number;
66221}