@chris.troutner/ipfs-message-port-client
Version:
IPFS client library for accessing IPFS node over message port
453 lines (413 loc) • 12.5 kB
JavaScript
'use strict'
/* eslint-env browser */
const Client = require('./client')
const CID = require('cids')
const { encodeCID, decodeCID } = require('@chris.troutner/ipfs-message-port-protocol/src/cid')
const {
decodeIterable,
encodeIterable,
encodeCallback
} = require('@chris.troutner/ipfs-message-port-protocol/src/core')
/** @type {<T>(stream:ReadableStream<T>) => AsyncIterable<T>} */
// @ts-ignore - browser-stream-to-it has no types
const iterateReadableStream = require('browser-readablestream-to-it')
const {
parseMode,
parseMtime
} = require('ipfs-unixfs')
/**
* @template T
* @typedef {import('@chris.troutner/ipfs-message-port-protocol/src/core').RemoteIterable<T>} RemoteIterable
*/
/**
* @typedef {import('@chris.troutner/ipfs-message-port-protocol/src/cid').EncodedCID} EncodedCID
* @typedef {import('@chris.troutner/ipfs-message-port-protocol/src/root').EncodedAddInput} EncodedAddInput
* @typedef {import('@chris.troutner/ipfs-message-port-protocol/src/root').EncodedAddAllInput} EncodedAddAllInput
* @typedef {import('@chris.troutner/ipfs-message-port-protocol/src/root').EncodedAddResult} EncodedAddResult
* @typedef {import('@chris.troutner/ipfs-message-port-protocol/src/root').EncodedIPFSEntry} EncodedIPFSEntry
* @typedef {import('@chris.troutner/ipfs-message-port-protocol/src/root').EncodedFileInput} EncodedFileInput
* @typedef {import('@chris.troutner/ipfs-message-port-protocol/src/root').EncodedFileContent} EncodedFileContent
* @typedef {import('@chris.troutner/ipfs-message-port-protocol/src/root').EncodedDirectoryInput} EncodedDirectoryInput
*
* @typedef {import('@chris.troutner/ipfs-message-port-server').CoreService} CoreService
*
* @typedef {import('./client').MessageTransport} MessageTransport
* @typedef {import('./interface').MessagePortClientOptions} MessagePortClientOptions
* @typedef {import('@chris.troutner/ipfs-core-types/src/root').API<MessagePortClientOptions>} RootAPI
*
* @typedef {import('@chris.troutner/ipfs-core-types/src/utils').ImportCandidate} ImportCandidate
* @typedef {import('@chris.troutner/ipfs-core-types/src/utils').ToFile} ToFile
* @typedef {import('@chris.troutner/ipfs-core-types/src/utils').ToDirectory} ToDirectory
* @typedef {import('@chris.troutner/ipfs-core-types/src/utils').ToContent} ToContent
* @typedef {import('@chris.troutner/ipfs-core-types/src/utils').ImportCandidateStream} ImportCandidateStream
*/
/**
* @class
* @extends {Client<CoreService>}
*/
class CoreClient extends Client {
/**
* @param {MessageTransport} transport
*/
constructor (transport) {
super('core', ['add', 'addAll', 'cat', 'ls'], transport)
}
}
/**
* Import files and data into IPFS.
*
* If you pass binary data like `Uint8Array` it is recommended to provide
* `transfer: [input.buffer]` which would allow transferring it instead of
* copying.
*
* @type {RootAPI["addAll"]}
*/
CoreClient.prototype.addAll = async function * addAll (input, options = {}) {
const { timeout, signal } = options
const transfer = [...(options.transfer || [])]
const progressCallback = options.progress
? encodeCallback(options.progress, transfer)
: undefined
const result = await this.remote.addAll({
...options,
input: encodeAddAllInput(input, transfer),
progress: undefined,
progressCallback,
transfer,
timeout,
signal
})
yield * decodeIterable(result.data, decodeAddedData)
}
/**
* Add file to IPFS.
*
* If you pass binary data like `Uint8Array` it is recommended to provide
* `transfer: [input.buffer]` which would allow transferring it instead of
* copying.
*
* @type {RootAPI["add"]}
*/
CoreClient.prototype.add = async function add (input, options = {}) {
const { timeout, signal } = options
const transfer = [...(options.transfer || [])]
const progressCallback = options.progress
? encodeCallback(options.progress, transfer)
: undefined
const result = await this.remote.add({
...options,
input: encodeAddInput(input, transfer),
progress: undefined,
progressCallback,
transfer,
timeout,
signal
})
return decodeAddedData(result.data)
}
/**
* Returns content addressed by a valid IPFS Path.
*
* @type {RootAPI["cat"]}
*/
CoreClient.prototype.cat = async function * cat (inputPath, options = {}) {
const input = CID.isCID(inputPath) ? encodeCID(inputPath) : inputPath
const result = await this.remote.cat({ ...options, path: input })
yield * decodeIterable(result.data, identity)
}
/**
* Returns content addressed by a valid IPFS Path.
*
* @type {RootAPI["ls"]}
*/
CoreClient.prototype.ls = async function * ls (inputPath, options = {}) {
const input = CID.isCID(inputPath) ? encodeCID(inputPath) : inputPath
const result = await this.remote.ls({ ...options, path: input })
yield * decodeIterable(result.data, decodeLsEntry)
}
/**
* Decodes values yield by `ipfs.add`.
*
* @param {EncodedAddResult} data
* @returns {import('@chris.troutner/ipfs-core-types/src/root').AddResult}
*/
const decodeAddedData = ({ path, cid, mode, mtime, size }) => {
return {
path,
cid: decodeCID(cid),
mode,
mtime,
size
}
}
/**
* @param {EncodedIPFSEntry} encodedEntry
* @returns {import('@chris.troutner/ipfs-core-types/src/root').IPFSEntry}
*/
const decodeLsEntry = ({ depth, name, path, size, cid, type, mode, mtime }) => ({
cid: decodeCID(cid),
type,
name,
path,
mode,
mtime,
size,
depth
})
/**
* @template T
* @param {T} v
* @returns {T}
*/
const identity = (v) => v
/**
* Encodes input passed to the `ipfs.add` via the best possible strategy for the
* given input.
*
* @param {ImportCandidate} input
* @param {Transferable[]} transfer
* @returns {EncodedAddInput}
*/
const encodeAddInput = (input, transfer) => {
// We want to get a Blob as input. If we got it we're set.
if (input instanceof Blob) {
return input
} else if (typeof input === 'string') {
return input
} else if (input instanceof ArrayBuffer) {
return input
} else if (ArrayBuffer.isView(input)) {
// Note we are not adding `input.buffer` into transfer list, it's on user.
return input
} else {
// If input is (async) iterable or `ReadableStream` or "FileObject" it will
// be encoded via own specific encoder.
const iterable = asIterable(input)
if (iterable) {
return encodeIterable(iterable, encodeIterableContent, transfer)
}
const asyncIterable = asAsyncIterable(input)
if (asyncIterable) {
return encodeIterable(
asyncIterable,
encodeAsyncIterableContent,
transfer
)
}
const readableStream = asReadableStream(input)
if (readableStream) {
return encodeIterable(
iterateReadableStream(readableStream),
encodeAsyncIterableContent,
transfer
)
}
const file = asFileObject(input)
if (file) {
return encodeFileObject(file, transfer)
}
throw TypeError('Unexpected input: ' + typeof input)
}
}
/**
* Encodes input passed to the `ipfs.add` via the best possible strategy for the
* given input.
*
* @param {ImportCandidateStream} input
* @param {Transferable[]} transfer
* @returns {EncodedAddAllInput}
*/
const encodeAddAllInput = (input, transfer) => {
// If input is (async) iterable or `ReadableStream` or "FileObject" it will
// be encoded via own specific encoder.
const iterable = asIterable(input)
if (iterable) {
return encodeIterable(iterable, encodeIterableContent, transfer)
}
const asyncIterable = asAsyncIterable(input)
if (asyncIterable) {
return encodeIterable(
asyncIterable,
encodeAsyncIterableContent,
transfer
)
}
const readableStream = asReadableStream(input)
if (readableStream) {
return encodeIterable(
iterateReadableStream(readableStream),
encodeAsyncIterableContent,
transfer
)
}
throw TypeError('Unexpected input: ' + typeof input)
}
/**
* Function encodes individual item of some `AsyncIterable` by choosing most
* effective strategy.
*
* @param {ImportCandidate} content
* @param {Transferable[]} transfer
* @returns {EncodedAddInput}
*/
const encodeAsyncIterableContent = (content, transfer) => {
if (content instanceof ArrayBuffer) {
return content
} else if (ArrayBuffer.isView(content)) {
return content
} else if (content instanceof Blob) {
return { path: '', content }
} else if (typeof content === 'string') {
return { path: '', content }
} else {
const file = asFileObject(content)
if (file) {
return encodeFileObject(file, transfer)
} else {
throw TypeError('Unexpected input: ' + typeof content)
}
}
}
/**
* @param {ImportCandidate} content
* @param {Transferable[]} transfer
* @returns {EncodedAddInput}
*/
const encodeIterableContent = (content, transfer) => {
if (typeof content === 'number') {
throw TypeError('Iterable of numbers is not supported')
} else if (content instanceof ArrayBuffer) {
return content
} else if (ArrayBuffer.isView(content)) {
return content
} else if (content instanceof Blob) {
return { path: '', content }
} else if (typeof content === 'string') {
return { path: '', content }
} else {
const file = asFileObject(content)
if (file) {
return encodeFileObject(file, transfer)
} else {
throw TypeError('Unexpected input: ' + typeof content)
}
}
}
/**
* @param {ToFile | ToDirectory} file
* @param {Transferable[]} transfer
* @returns {EncodedFileInput | EncodedDirectoryInput}
*/
const encodeFileObject = ({ path, mode, mtime, content }, transfer) => {
/** @type {any} */
const output = {
path,
mode: parseMode(mode),
mtime: parseMtime(mtime)
}
if (content) {
output.content = encodeFileContent(content, transfer)
}
return output
}
/**
* @param {ToContent|undefined} content
* @param {Transferable[]} transfer
* @returns {EncodedFileContent}
*/
const encodeFileContent = (content, transfer) => {
if (content == null) {
return ''
} else if (content instanceof ArrayBuffer || ArrayBuffer.isView(content)) {
return content
} else if (content instanceof Blob) {
return content
} else if (typeof content === 'string') {
return content
} else {
const iterable = asIterable(content)
if (iterable) {
return encodeIterable(iterable, encodeIterableContent, transfer)
}
const asyncIterable = asAsyncIterable(content)
if (asyncIterable) {
return encodeIterable(
asyncIterable,
encodeAsyncIterableContent,
transfer
)
}
const readableStream = asReadableStream(content)
if (readableStream) {
return encodeIterable(
iterateReadableStream(readableStream),
encodeAsyncIterableContent,
transfer
)
}
throw TypeError('Unexpected input: ' + typeof content)
}
}
/**
* Pattern matches given input as `Iterable<I>` and returns back either matched
* iterable or `null`.
*
* @template I
* @param {Iterable<I>|ImportCandidate|ImportCandidateStream} input
* @returns {Iterable<I>|null}
*/
const asIterable = (input) => {
/** @type {*} */
const object = input
if (object && typeof object[Symbol.iterator] === 'function') {
return object
} else {
return null
}
}
/**
* Pattern matches given `input` as `AsyncIterable<I>` and returns back either
* matched `AsyncIterable` or `null`.
*
* @template I
* @param {AsyncIterable<I>|ImportCandidate|ImportCandidateStream} input
* @returns {AsyncIterable<I>|null}
*/
const asAsyncIterable = (input) => {
/** @type {*} */
const object = input
if (object && typeof object[Symbol.asyncIterator] === 'function') {
return object
} else {
return null
}
}
/**
* Pattern matches given `input` as `ReadableStream` and return back either
* matched input or `null`.
*
* @param {any} input
* @returns {ReadableStream<Uint8Array>|null}
*/
const asReadableStream = (input) => {
if (input && typeof input.getReader === 'function') {
return input
} else {
return null
}
}
/**
* Pattern matches given input as "FileObject" and returns back eithr matched
* input or `null`.
*
* @param {*} input
* @returns {ToFile|null}
*/
const asFileObject = (input) => {
if (typeof input === 'object' && (input.path || input.content)) {
return input
} else {
return null
}
}
module.exports = CoreClient