From 9bd31ff2345187ccd07770d5e935d8b171638f21 Mon Sep 17 00:00:00 2001 From: Lucas Tortora Date: Tue, 30 Jun 2026 23:36:40 -0300 Subject: [PATCH] chore: add JSDoc to public API Generated JSDoc (descriptions, typed @param/@returns, @throws, @typedef option shapes) for the public API surface. Co-Authored-By: Claude Opus 4.8 --- index.js | 448 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 448 insertions(+) diff --git a/index.js b/index.js index 94af115d..da477eb8 100644 --- a/index.js +++ b/index.js @@ -17,7 +17,74 @@ const keyEncoding = new SubEncoder('files', 'utf-8') const [BLOBS] = crypto.namespace('hyperdrive', 1) +/** + * Options for creating a Hyperdrive. + * @typedef {Object} DriveOptions + * @property {Buffer} [encryptionKey] - Encryption key used to encrypt and decrypt the underlying cores. + * @property {Function} [onwait] - Hook called when a block has to be downloaded before it can be read. + * @property {boolean} [active=true] - Whether the drive's cores actively maintain peer connections. + */ + +/** + * Options for `drive.truncate()`. + * @typedef {Object} TruncateOptions + * @property {number} [blobs] - Corresponding blobs length, if known. Recommended to let the method figure it out. + */ + +/** + * Options for `drive.update()`. + * @typedef {Object} UpdateOptions + * @property {boolean} [wait=false] - Whether to wait for peers before resolving. Use this or `drive.findingPeers()` to make `await drive.update()` blocking. + */ + +/** + * Options for `drive.get()`. + * @typedef {Object} GetOptions + * @property {boolean} [wait=true] - Wait for the block to be downloaded. + * @property {number} [timeout=0] - Max milliseconds to wait (0 means no timeout). + */ + +/** + * Options for writing a file (`drive.put()` and `drive.createWriteStream()`). + * @typedef {Object} WriteOptions + * @property {boolean} [executable=false] - Whether the blob at the path is an executable. + * @property {*} [metadata=null] - Extended file information, i.e. an arbitrary JSON value. + * @property {boolean} [dedup=false] - Reuse the existing blob if the content is unchanged (createWriteStream only). + */ + +/** + * Options for clearing blobs (`drive.clear()` and `drive.clearAll()`). + * @typedef {Object} ClearOptions + * @property {boolean} [diff=false] - When enabled, returns a `{ blocks }` object reporting the blocks cleared; otherwise the result is `null`. + */ + +/** + * Options for `drive.symlink()`. + * @typedef {Object} SymlinkOptions + * @property {*} [metadata=null] - Extended file information, i.e. an arbitrary JSON value. + */ + +/** + * Options for `drive.entry()`. + * @typedef {Object} EntryOptions + * @property {boolean} [follow=false] - Follow symlinks, up to 16 deep or it throws an error. + * @property {boolean} [wait=true] - Wait for the block to be downloaded. + * @property {number} [timeout=0] - Max milliseconds to wait (0 means no timeout). + */ + module.exports = class Hyperdrive extends ReadyResource { + /** + * Creates a new Hyperdrive instance. `store` must be an instance of + * `Corestore`. + * @param {Corestore} corestore + * @param {Buffer} [key] - Public key of an existing drive to load. May be omitted, passing `opts` in its place. + * @param {DriveOptions} [opts] - Drive options. + * @example + * const Corestore = require('corestore') + * + * const store = new Corestore('./storage') + * const drive = new Hyperdrive(store) + */ constructor(corestore, key, opts = {}) { super() @@ -26,10 +93,26 @@ module.exports = class Hyperdrive extends ReadyResource { key = null } + /** + * The Corestore instance used as storage. + * @type {Corestore} + */ this.corestore = corestore + /** + * The underlying Hyperbee backing the drive file structure. + * @type {Hyperbee} + */ this.db = opts._db || makeBee(key, corestore, opts) + /** + * The Hypercore used for `drive.db`. + * @type {Hypercore} + */ this.core = this.db.core this.blobs = null + /** + * Boolean indicating if the drive handles or not metadata. Always `true`. + * @type {boolean} + */ this.supportsMetadata = true this.encryptionKey = opts.encryptionKey || null this.monitors = new Set() @@ -80,38 +163,89 @@ module.exports = class Hyperdrive extends ReadyResource { return generateContentManifest(m, this.core.key) } + /** + * String containing the id (z-base-32 of the public key) identifying this + * drive. + * @returns {string} + */ get id() { return this.core.id } + /** + * The public key of the Hypercore backing the drive. + * @returns {Buffer} + */ get key() { return this.core.key } + /** + * The hash of the public key of the Hypercore backing the drive. + * @returns {Buffer} + */ get discoveryKey() { return this.core.discoveryKey } + /** + * The public key of the + * [Hyperblobs](https://github.com/holepunchto/hyperblobs) instance holding + * blobs associated with entries in the drive. + * @returns {Buffer} + */ get contentKey() { return this.blobs?.core.key } + /** + * Number that indicates how many modifications were made, useful as a version + * identifier. + * @returns {number} + */ get version() { return this.db.version } + /** + * Boolean indicating if we can write or delete data in this drive. + * @returns {boolean} + */ get writable() { return this.core.writable } + /** + * Boolean indicating if we can read from this drive. After closing the drive + * this will be `false`. + * @returns {boolean} + */ get readable() { return this.core.readable } + /** + * Indicate to Hyperdrive that you're finding peers in the background, requests + * will be on hold until this is done. + * @returns {Function} A `done` callback to call once peer discovery has finished. + * @example + * const done = drive.findingPeers() + * swarm.flush().then(done, done) + */ findingPeers() { return this.corestore.findingPeers() } + /** + * Truncates the Hyperdrive to a previous version (both the file-structure + * reference and the blobs). + * @param {number} version - Drive version to truncate to. + * @param {TruncateOptions} [options] - Truncate options. + * @returns {Promise} Resolves once the drive and its blobs are truncated. + * @throws {BAD_ARGUMENT} if the truncation length is invalid. + * @example + * await drive.truncate(drive.version - 1) + */ async truncate(version, { blobs = -1 } = {}) { if (!this.opened) await this.ready() @@ -130,6 +264,14 @@ module.exports = class Hyperdrive extends ReadyResource { await bl.core.truncate(blobsVersion) } + /** + * Returns the length of the Hyperblobs instance at the time of the specified + * Hyperdrive version (defaults to the current version). + * @param {number} [checkout] - Drive version to measure. Defaults to the current version. + * @returns {Promise} the length of the Hyperblobs instance at the time of the specified Hyperdrive version (defaults to the current version). + * @example + * const blobsLength = await drive.getBlobsLength() + */ async getBlobsLength(checkout) { if (!this.opened) await this.ready() @@ -144,10 +286,36 @@ module.exports = class Hyperdrive extends ReadyResource { } } + /** + * Creates a replication stream for the drive. Pass `true`/`false` to create a + * new stream as initiator/responder, or pass an existing stream or socket to + * replicate over it. See + * [`corestore.replicate`](https://github.com/holepunchto/corestore#const-stream--storereplicateoptsorstream) + * for how replication works. + * @param {boolean|Stream} isInitiator - `true`/`false` to initiate replication, or an existing stream/socket to replicate over. + * @param {object} opts - Replication options, forwarded to `corestore.replicate()`. + * @returns {Stream} A replication stream. + * @example + * const swarm = new Hyperswarm() + * const done = drive.findingPeers() + * swarm.on('connection', (socket) => drive.replicate(socket)) + * swarm.join(drive.discoveryKey) + * swarm.flush().then(done, done) + */ replicate(isInitiator, opts) { return this.corestore.replicate(isInitiator, opts) } + /** + * Waits for initial proof of the new drive version until all `findingPeers` + * are done. + * @param {UpdateOptions} [opts] + * @returns {Promise} `true` if the drive advanced to a new version, otherwise `false`. + * @example + * { + * wait: false + * } + */ update(opts) { return this.db.update(opts) } @@ -162,10 +330,26 @@ module.exports = class Hyperdrive extends ReadyResource { }) } + /** + * Get a read-only snapshot of a previous version. + * @param {number} version - Drive version to snapshot. + * @returns {Hyperdrive} A read-only Hyperdrive at the requested version. + * @example + * const snapshot = drive.checkout(4) + */ checkout(version) { return this._makeCheckout(this.db.checkout(version)) } + /** + * Useful for atomically mutate the drive, has the same interface as + * Hyperdrive. + * @returns {Hyperdrive} A batch with the same interface as Hyperdrive; call `batch.flush()` to commit. + * @example + * const batch = drive.batch() + * await batch.put('/a.txt', Buffer.from('a')) + * await batch.flush() + */ batch() { return new Hyperdrive(this.corestore, this.key, { onwait: this._onwait, @@ -184,6 +368,12 @@ module.exports = class Hyperdrive extends ReadyResource { if (this.blobs) this.blobs.core.setActive(active) } + /** + * Commit a batch of mutations to the underlying drive. + * @returns {Promise} Resolves once the batch is committed and closed. + * @example + * await batch.flush() + */ async flush() { await this.db.flush() return this.close() @@ -288,6 +478,21 @@ module.exports = class Hyperdrive extends ReadyResource { } } + /** + * Returns the [Hyperblobs](https://github.com/holepunchto/hyperblobs) + * instance storing the blobs indexed by drive entries. + * @returns {Promise} the [Hyperblobs](https://github.com/holepunchto/hyperblobs) instance storing the blobs indexed by drive entries. + * @example + * await drive.put('/file.txt', Buffer.from('hi')) + * + * const buffer1 = await drive.get('/file.txt') + * + * const blobs = await drive.getBlobs() + * const entry = await drive.entry('/file.txt') + * const buffer2 = await blobs.get(entry.value.blob) + * + * // => buffer1 and buffer2 are equals + */ async getBlobs() { if (this.blobs) return this.blobs @@ -313,6 +518,20 @@ module.exports = class Hyperdrive extends ReadyResource { await Promise.allSettled(closing) } + /** + * Reads the blob stored at `path` and resolves with its contents as a + * `Buffer`. Resolves with `null` when no blob exists at `path`, and also + * returns `null` for symbolic links. + * @param {string} name - Path of the file to read. + * @param {GetOptions} [opts] + * @returns {Promise} The blob at `path`, or `null` if the path does not exist or is a symbolic link. + * @throws {BLOCK_NOT_AVAILABLE} if a required block is not available. + * @example + * { + * wait: true, // Wait for block to be downloaded + * timeout: 0 // Wait at max some milliseconds (0 means no timeout) + * } + */ async get(name, opts) { const node = await this.entry(name, opts) if (!node?.value.blob) return null @@ -327,6 +546,16 @@ module.exports = class Hyperdrive extends ReadyResource { return this.db.put(std(name, false), { executable, linkname, blob, metadata }, { keyEncoding }) } + /** + * Creates a file at `path` in the drive. `options` are the same as in + * `createWriteStream`. + * @param {string} name - Path to write the file at. + * @param {Buffer} buf - Blob contents to store. + * @param {WriteOptions} [options] + * @returns {Promise} Resolves once the file has been written. + * @example + * await drive.put('/blob.txt', Buffer.from('example')) + */ async put(name, buf, { executable = false, metadata = null } = {}) { await this.getBlobs() const blob = await this.blobs.put(buf) @@ -337,15 +566,41 @@ module.exports = class Hyperdrive extends ReadyResource { ) } + /** + * Deletes the file at `path` from the drive. + * @param {string} name - Path of the file to delete. + * @returns {Promise} Resolves once the entry has been deleted. + * @example + * await drive.del('/blob.txt') + */ async del(name) { return this.db.del(std(name, false), { keyEncoding }) } + /** + * Compares two entries by their sequence number. + * @param {object} a - The first entry (as returned by `drive.entry()`). + * @param {object} b - The second entry (as returned by `drive.entry()`). + * @returns {number} `0` if entries are the same, `1` if `entryA` is newer, and `-1` if `entryB` is newer. + * @example + * const comparison = drive.compare(entryA, entryB) + */ compare(a, b) { const diff = a.seq - b.seq return diff > 0 ? 1 : diff < 0 ? -1 : 0 } + /** + * Deletes the blob from storage to free up space, but the file structure + * reference is kept. + * @param {string} name - Path of the file whose blob should be cleared. + * @param {ClearOptions} [opts] + * @returns {Promise} A `{ blocks }` object when `diff` is enabled, otherwise `null`. + * @example + * { + * diff: false // Returned `cleared` bytes object is null unless you enable this + * } + */ async clear(name, opts) { if (!this.opened) await this.ready() @@ -364,6 +619,16 @@ module.exports = class Hyperdrive extends ReadyResource { return this.blobs.clear(node.value.blob, opts) } + /** + * Deletes all the blobs from storage to free up space, similar to how + * `drive.clear()` works. + * @param {ClearOptions} [opts] + * @returns {Promise} A `{ blocks }` object when `diff` is enabled, otherwise `null`. + * @example + * { + * diff: false // Returned `cleared` bytes object is null unless you enable this + * } + */ async clearAll(opts) { if (!this.opened) await this.ready() @@ -374,6 +639,14 @@ module.exports = class Hyperdrive extends ReadyResource { return this.blobs.core.clear(0, this.blobs.core.length, opts) } + /** + * Purge both cores (db and blobs) from your storage, completely removing all + * the drive's data. + * @returns {Promise} Resolves once both cores are purged from storage. + * @throws if called on a non-main session — only the main session can be purged. + * @example + * await drive.purge() + */ async purge() { if (this._checkout || this._batch) throw new Error('Can only purge the main session') @@ -385,6 +658,15 @@ module.exports = class Hyperdrive extends ReadyResource { await Promise.all(proms) } + /** + * Creates an entry in drive at `path` that points to the entry at `linkname`. + * @param {string} name - Path of the symlink to create. + * @param {string} dst - Path the symlink points to. + * @param {SymlinkOptions} [options] - Symlink options. + * @returns {Promise} Resolves once the symlink entry has been created. + * @example + * await drive.symlink('/images/logo.shortcut', '/images/logo.png') + */ async symlink(name, dst, { metadata = null } = {}) { return this.db.put( std(name, false), @@ -393,6 +675,29 @@ module.exports = class Hyperdrive extends ReadyResource { ) } + /** + * Resolves with the entry stored at `path`, or `null` if no entry exists. + * @param {string} name - Path of the entry to read. + * @param {EntryOptions} [opts] + * @returns {Promise} the entry at `path` in the drive. + * @throws if a symlink chain is recursive or exceeds 16 hops when `follow` is enabled. + * @example + * { + * seq: Number, + * key: String, + * value: { + * executable: Boolean, // Whether the blob at path is an executable + * linkname: null, // If entry not symlink, otherwise a string to the entry this links to + * blob: { // Hyperblobs id that can be used to fetch the blob associated with this entry + * blockOffset: Number, + * blockLength: Number, + * byteOffset: Number, + * byteLength: Number + * }, + * metadata: null + * } + * } + */ async entry(name, opts) { if (!opts || !opts.follow) return this._entry(name, opts) @@ -412,10 +717,30 @@ module.exports = class Hyperdrive extends ReadyResource { return this.db.get(std(name, false), { ...opts, keyEncoding }) } + /** + * Checks whether an entry exists at `path`. + * @param {string} name - Path to check. + * @returns {Promise} `true` if the entry at `path` does exists, otherwise `false`. + * @example + * const exists = await drive.exists('/blob.txt') + */ async exists(name) { return (await this.entry(name)) !== null } + /** + * Returns an async iterator that watches `folder` (defaults to `/`) and yields + * `[current, previous]` snapshot pairs whenever the drive changes. The + * snapshots are auto-closed before the next value, so do not close them + * yourself. + * @param {string} [folder] - to watch. Defaults to `/`. + * @returns {Watcher} an iterator that listens on `folder` to yield changes, by default on `/`. + * @example + * for await (const [current, previous] of watcher) { + * console.log(current.version) + * console.log(previous.version) + * } + */ watch(folder) { folder = std(folder || '/', true) @@ -425,6 +750,19 @@ module.exports = class Hyperdrive extends ReadyResource { }) } + /** + * Efficiently create a stream of the changes to `folder` between `version` and + * `drive.version`. + * @param {number} length - Drive version to diff against the current `drive.version`. + * @param {string} folder - Path prefix to scope the diff to. Omit for the whole drive. + * @param {object} [opts] - Diff options, forwarded to the underlying Hyperbee diff stream. + * @returns {Readable} A stream of `{ left, right }` diff entries. + * @example + * { + * left: Object, // Entry in folder at drive.version for some path + * right: Object, // Entry in folder at drive.checkout(version) for some path + * } + */ diff(length, folder, opts) { if (typeof folder === 'object' && folder && !opts) return this.diff(length, null, folder) @@ -436,6 +774,18 @@ module.exports = class Hyperdrive extends ReadyResource { }) } + /** + * Downloads all the blobs in `folder` corresponding to entries in + * `drive.checkout(version)` that are not in `drive.version`. Returns a + * `Download` object that resolves once all data has been downloaded: + * @param {number} length - Drive version to diff against the current `drive.version`. + * @param {string} folder - Path prefix to scope the download to. Omit for the whole drive. + * @param {object} [opts] - Diff options, forwarded to `drive.diff()`. + * @returns {Download} A Download object; await `download.done()` to know when complete. + * @example + * const download = await drive.downloadDiff(version, folder) + * await download.done() + */ async downloadDiff(length, folder, opts) { const dls = [] @@ -450,6 +800,17 @@ module.exports = class Hyperdrive extends ReadyResource { return new Download(this, null, { downloads: dls }) } + /** + * Downloads the entries and blobs stored in the ranges `dbRanges` and + * `blobRanges`. Returns a `Download` object that resolves once all data has + * been downloaded: + * @param {Array} dbRanges - Block ranges to download from the db (metadata) core. + * @param {Array} blobRanges - Block ranges to download from the blobs core. + * @returns {Download} A Download object; await `download.done()` to know when complete. + * @example + * const download = await drive.downloadRange(dbRanges, blobRanges) + * await download.done() + */ async downloadRange(dbRanges, blobRanges) { const dls = [] @@ -468,18 +829,45 @@ module.exports = class Hyperdrive extends ReadyResource { return new Download(this, null, { downloads: dls }) } + /** + * Returns a read stream of the drive entries within the given Hyperbee + * `range`. + * @param {object} [range] - Hyperbee range bounds (`gt`/`gte`/`lt`/`lte`). + * @param {object} [opts] + * @returns {Readable} a read stream of entries in the drive. + * @example + * for await (const entry of drive.entries()) console.log(entry.key) + */ entries(range, opts) { const stream = this.db.createReadStream(range, { ...opts, keyEncoding }) if (opts && opts.ignore) stream._readableState.map = createStreamMapIgnore(opts.ignore) return stream } + /** + * Downloads the blobs corresponding to all entries in the drive at paths + * prefixed with `folder`. Returns a `Download` object that resolves once all + * data has been downloaded: + * @param {string} [folder] - Path prefix to download. Defaults to the whole drive. + * @param {object} [opts] - `options` are the same as those for `drive.list(folder, [options])`. + * @returns {Download} A Download object; await `download.done()` to know when complete. + * @example + * const download = await drive.download(key) + * await download.done() + */ download(folder = '/', opts) { if (typeof folder === 'object') return this.download(undefined, folder) return new Download(this, folder, opts) } + /** + * Checks if path is saved to local store already. + * @param {string} path - of the file or folder to check. + * @returns {Promise} `true` if the blob (or every blob under a folder) is stored locally. + * @example + * const exists = await drive.has('/blob.txt') + */ async has(path) { const blobs = await this.getBlobs() const entry = !path || path.endsWith('/') ? null : await this.entry(path) @@ -500,6 +888,19 @@ module.exports = class Hyperdrive extends ReadyResource { } // atm always recursive, but we should add some depth thing to it + /** + * Returns a stream of all entries in the drive at paths prefixed with + * `folder`. + * @param {string} folder - Path prefix to list. Defaults to the whole drive. + * @param {object} [opts] + * @returns {Readable} a stream of all entries in the drive at paths prefixed with `folder`. + * @example + * { + * recursive: true | false // Whether to descend into all subfolders or not, + * ignore: String || Array // Ignore files and folders by name, + * wait: true, // Wait for block to be downloaded. + * } + */ list(folder, opts = {}) { if (typeof folder === 'object' && folder !== null) return this.list(undefined, folder) @@ -513,15 +914,51 @@ module.exports = class Hyperdrive extends ReadyResource { return stream } + /** + * Returns a stream of the immediate subpaths of entries stored at paths + * prefixed by `folder`. + * @param {string} folder - Path prefix to read. + * @param {object} [opts] + * @returns {Readable} a stream of all subpaths of entries in drive stored at paths prefixed by `folder`. + * @example + * { + * wait: true, // Wait for block to be downloaded + * } + */ readdir(folder, opts) { folder = std(folder || '/', true) return shallowReadStream(this.db, folder, true, null, opts) } + /** + * Efficiently mirror this drive into another. Returns a + * [`MirrorDrive`](https://github.com/holepunchto/mirror-drive#api) instance + * constructed with `options`. + * @param {Hyperdrive|Localdrive} out - Destination drive to mirror into. + * @param {object} [opts] - Mirror options, forwarded to `MirrorDrive`. + * @returns {MirrorDrive} A `MirrorDrive` instance; await it or `mirror.done()` to finish. + * @example + * const mirror = drive.mirror(out) + * await mirror.done() + */ mirror(out, opts) { return new MirrorDrive(this, out, opts) } + /** + * Returns a readable stream of the blob stored in the drive at `path`. + * @param {string} name - Path of the file to read. + * @param {object} [opts] + * @returns {Readable} a stream to read out the blob stored in the drive at `path`. + * @example + * { + * start: Number, // `start` and `end` are inclusive + * end: Number, + * length: Number, // `length` overrides `end`, they're not meant to be used together + * wait: true, // Wait for blocks to be downloaded + * timeout: 0 // Wait at max some milliseconds (0 means no timeout) + * } + */ createReadStream(name, opts) { const self = this @@ -576,6 +1013,17 @@ module.exports = class Hyperdrive extends ReadyResource { return stream } + /** + * Stream a blob into the drive at `path`. + * @param {string} name - Path of the file to write. + * @param {WriteOptions} [options] - Write options — the same as for `drive.put(path, buffer, [options])`. + * @returns {Writable} A writable stream; data written is stored as the file's blob. + * @example + * { + * executable: Boolean, + * metadata: null // Extended file information i.e. arbitrary JSON value + * } + */ createWriteStream(name, { executable = false, metadata = null, dedup = false } = {}) { const self = this