diff --git a/README.md b/README.md index e286fa3..4ffeec9 100644 --- a/README.md +++ b/README.md @@ -114,6 +114,20 @@ if (slots.error) { // Access the slots within slots.data. ``` +If you prefer to use the "classic" JavaScript mode and deal with exceptions, you can import the throwable component instead: + +```js +import { Codex } from "@codex-storage/sdk-js/throwable"; + +const marketplace = codex.marketplace; + +try { + const slots = marketplace.activeSlots(); +} catch (e) { + // Do something +} +``` + ### Compatibility | SDK version | Codex version | Codex app | diff --git a/examples/upload-node-throwable/README.md b/examples/upload-node-throwable/README.md new file mode 100644 index 0000000..d1ebb90 --- /dev/null +++ b/examples/upload-node-throwable/README.md @@ -0,0 +1,17 @@ +# Download example + +Small example to show how to download a file in the browser with Codex. + +## Install dependencies + +```bash +npm install +``` + +## Run node + +```bash +node index.js +``` + +Note: You can define `CODEX_NODE_URL`, default value is "http://localhost:8080". diff --git a/examples/upload-node-throwable/index.js b/examples/upload-node-throwable/index.js new file mode 100644 index 0000000..76d2334 --- /dev/null +++ b/examples/upload-node-throwable/index.js @@ -0,0 +1,18 @@ +const { Codex } = require("@codex-storage/sdk-js/throwable"); +const { NodeUploadStrategy } = require("@codex-storage/sdk-js/node"); + +async function main() { + const codex = new Codex( + process.env.CODEX_NODE_URL || "http://localhost:8080" + ); + const data = codex.data; + + const strategy = new NodeUploadStrategy("Hello World !"); + const uploadResponse = data.upload(strategy); + + const cid = await uploadResponse.result; + + console.info("CID is", cid); +} + +main(); diff --git a/examples/upload-node-throwable/package-lock.json b/examples/upload-node-throwable/package-lock.json new file mode 100644 index 0000000..eb7f034 --- /dev/null +++ b/examples/upload-node-throwable/package-lock.json @@ -0,0 +1,79 @@ +{ + "name": "@codex-storage/sdk-js-update-node-throwable-example", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@codex-storage/sdk-js-update-node-throwable-example", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@codex-storage/sdk-js": "../..", + "undici": "^7.7.0" + }, + "devDependencies": { + "prettier": "^3.5.3" + } + }, + "..": { + "extraneous": true + }, + "../..": { + "name": "@codex-storage/sdk-js", + "version": "0.1.2", + "license": "MIT", + "dependencies": { + "valibot": "^1.0.0" + }, + "devDependencies": { + "@tsconfig/strictest": "^2.0.5", + "@types/node": "^22.13.17", + "oas-normalize": "^14.0.0", + "openapi-typescript": "^7.6.1", + "prettier": "^3.5.3", + "tsup": "^8.3.6", + "typescript": "^5.8.2", + "vitest": "^3.1.1" + }, + "engines": { + "node": ">=20.18.1" + }, + "peerDependencies": { + "undici": "^7.7.0" + } + }, + "../dist": { + "extraneous": true + }, + "node_modules/@codex-storage/sdk-js": { + "resolved": "../..", + "link": true + }, + "node_modules/prettier": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", + "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/undici": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.7.0.tgz", + "integrity": "sha512-tZ6+5NBq4KH35rr46XJ2JPFKxfcBlYNaqLF/wyWIO9RMHqqU/gx/CLB1Y2qMcgB8lWw/bKHa7qzspqCN7mUHvA==", + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + } + } +} diff --git a/examples/upload-node-throwable/package.json b/examples/upload-node-throwable/package.json new file mode 100644 index 0000000..0791f7b --- /dev/null +++ b/examples/upload-node-throwable/package.json @@ -0,0 +1,16 @@ +{ + "name": "@codex-storage/sdk-js-update-node-throwable-example", + "version": "1.0.0", + "main": "index.js", + "scripts": {}, + "author": "", + "license": "ISC", + "description": "", + "dependencies": { + "@codex-storage/sdk-js": "../..", + "undici": "^7.7.0" + }, + "devDependencies": { + "prettier": "^3.5.3" + } +} \ No newline at end of file diff --git a/package.json b/package.json index a821333..79911b0 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "scripts": { "prepack": "npm run build", "prebuild": "npm run compile && rm -Rf dist/*", - "build": "tsup src/index.ts src/async.ts src/browser.ts src/node.ts --format esm,cjs --dts --sourcemap --treeshake", + "build": "tsup src/index.ts src/async.ts src/browser.ts src/node.ts src/throwable.ts --format esm,cjs --dts --sourcemap --treeshake", "compile": "tsc --noEmit", "test": "vitest run", "test:watch": "vitest", @@ -35,6 +35,16 @@ "default": "./dist/index.js" } }, + "./throwable": { + "import": { + "types": "./dist/throwable.d.ts", + "default": "./dist/throwable.mjs" + }, + "require": { + "types": "./dist/throwable.d.cts", + "default": "./dist/throwable.js" + } + }, "./browser": { "import": { "types": "./dist/browser.d.ts", diff --git a/src/data/data.throwable.ts b/src/data/data.throwable.ts new file mode 100644 index 0000000..d70388a --- /dev/null +++ b/src/data/data.throwable.ts @@ -0,0 +1,33 @@ +import { CodexData } from "./data"; +import type { UploadStrategy } from "./types"; +import { type FetchAuth } from "../fetch-safe/fetch-safe"; +import { Throwable } from "../throwable/throwable"; + +type CodexDataThrowableOptions = { + auth?: FetchAuth; +}; + +export class CodexDataThrowable { + readonly data: CodexData; + + constructor(url: string, options?: CodexDataThrowableOptions) { + this.data = new CodexData(url, options); + } + + cids = () => Throwable.from(this.data.cids()); + space = () => Throwable.from(this.data.space()); + upload = (strategy: UploadStrategy) => { + const { result, abort } = this.data.upload(strategy); + return { + result: Throwable.from(result), + abort, + }; + }; + localDownload = (cid: string) => Throwable.from(this.data.localDownload(cid)); + networkDownload = (cid: string) => + Throwable.from(this.data.networkDownload(cid)); + networkDownloadStream = (cid: string) => + Throwable.from(this.data.networkDownloadStream(cid)); + fetchManifest = (cid: string) => Throwable.from(this.data.fetchManifest(cid)); + delete = (cid: string) => Throwable.from(this.data.delete(cid)); +} diff --git a/src/debug/debug.throwable.ts b/src/debug/debug.throwable.ts new file mode 100644 index 0000000..062bf24 --- /dev/null +++ b/src/debug/debug.throwable.ts @@ -0,0 +1,20 @@ +import type { CodexLogLevel } from "./types"; +import { type FetchAuth } from "../fetch-safe/fetch-safe"; +import { Throwable } from "../throwable/throwable"; +import { CodexDebug } from "./debug"; + +type CodexDebugThrowableOptions = { + auth?: FetchAuth; +}; + +export class CodexDebugThrowable { + readonly debug: CodexDebug; + + constructor(url: string, options?: CodexDebugThrowableOptions) { + this.debug = new CodexDebug(url, options); + } + + setLogLevel = (level: CodexLogLevel) => + Throwable.from(this.debug.setLogLevel(level)); + info = () => Throwable.from(this.debug.info()); +} diff --git a/src/marketplace/marketplace.throwable.ts b/src/marketplace/marketplace.throwable.ts new file mode 100644 index 0000000..84db9fa --- /dev/null +++ b/src/marketplace/marketplace.throwable.ts @@ -0,0 +1,37 @@ +import { type FetchAuth } from "../fetch-safe/fetch-safe"; +import { Throwable } from "../throwable/throwable"; +import { CodexMarketplace } from "./marketplace"; +import type { + CodexAvailabilityPatchInput, + CodexCreateAvailabilityInput, + CodexCreateStorageRequestInput, +} from "./types"; + +type CodexMarketplaceThrowableOptions = { + auth?: FetchAuth; +}; + +export class CodexMarketplaceThrowable { + readonly marketplace: CodexMarketplace; + + constructor(url: string, options?: CodexMarketplaceThrowableOptions) { + this.marketplace = new CodexMarketplace(url, options); + } + + activeSlots = () => Throwable.from(this.marketplace.activeSlots()); + activeSlot = (slotId: string) => + Throwable.from(this.marketplace.activeSlot(slotId)); + availabilities = () => Throwable.from(this.marketplace.availabilities()); + createAvailability = (input: CodexCreateAvailabilityInput) => + Throwable.from(this.marketplace.createAvailability(input)); + updateAvailability = (input: CodexAvailabilityPatchInput) => + Throwable.from(this.marketplace.updateAvailability(input)); + reservations = (availabilityId: string) => + Throwable.from(this.marketplace.reservations(availabilityId)); + purchaseIds = () => Throwable.from(this.marketplace.purchaseIds()); + purchases = () => Throwable.from(this.marketplace.purchases()); + purchaseDetail = (purchaseId: string) => + Throwable.from(this.marketplace.purchaseDetail(purchaseId)); + createStorageRequest = (input: CodexCreateStorageRequestInput) => + Throwable.from(this.marketplace.createStorageRequest(input)); +} diff --git a/src/node/node.throwable.ts b/src/node/node.throwable.ts new file mode 100644 index 0000000..2df7301 --- /dev/null +++ b/src/node/node.throwable.ts @@ -0,0 +1,23 @@ +import type { CodexPeerIdContentType, CodexSprContentType } from "./types"; +import { type FetchAuth } from "../fetch-safe/fetch-safe"; +import { Throwable } from "../throwable/throwable"; +import { CodexNode } from "./node"; + +type CodexNodeThrowableOptions = { + auth?: FetchAuth; +}; + +export class CodexNodeThrowable { + readonly node: CodexNode; + + constructor(url: string, options?: CodexNodeThrowableOptions) { + this.node = new CodexNode(url, options); + } + + connect = (peerId: string, addrs: string[] = []) => + Throwable.from(this.node.connect(peerId, addrs)); + spr = (type: CodexSprContentType = "json") => + Throwable.from(this.node.spr(type)); + peerId = (type: CodexPeerIdContentType = "json") => + Throwable.from(this.node.peerId(type)); +} diff --git a/src/throwable.ts b/src/throwable.ts new file mode 100644 index 0000000..4f121d1 --- /dev/null +++ b/src/throwable.ts @@ -0,0 +1,84 @@ +import { CodexDataThrowable } from "./data/data.throwable"; +import { CodexNodeThrowable } from "./node/node.throwable"; +import { CodexMarketplaceThrowable } from "./marketplace/marketplace.throwable"; +import { CodexDebugThrowable } from "./debug/debug.throwable"; +import type { FetchAuth } from "./fetch-safe/fetch-safe"; + +export * from "./fetch-safe/fetch-safe"; +export * from "./marketplace/types"; +export * from "./debug/types"; +export * from "./data/types"; +export * from "./values/values"; +export * from "./errors/errors"; + +export { CodexDebugThrowable } from "./debug/debug.throwable"; +export { CodexDataThrowable } from "./data/data.throwable"; +export { CodexNodeThrowable } from "./node/node.throwable"; +export { CodexMarketplaceThrowable } from "./marketplace/marketplace.throwable"; + +type CodexProps = { + auth?: FetchAuth; +}; + +export class Codex { + readonly url: string; + private _marketplace: CodexMarketplaceThrowable | null; + private _data: CodexDataThrowable | null; + private _node: CodexNodeThrowable | null; + private _debug: CodexDebugThrowable | null; + private readonly auth: FetchAuth = {}; + + constructor(url: string, options?: CodexProps) { + this.url = url; + this._marketplace = null; + this._data = null; + this._node = null; + this._debug = null; + + if (options?.auth) { + this.auth = options?.auth; + } + } + + get marketplace() { + if (this._marketplace) { + return this._marketplace; + } + + this._marketplace = new CodexMarketplaceThrowable(this.url, { + auth: this.auth, + }); + + return this._marketplace; + } + + get data() { + if (this._data) { + return this._data; + } + + this._data = new CodexDataThrowable(this.url, { auth: this.auth }); + + return this._data; + } + + get node() { + if (this._node) { + return this._node; + } + + this._node = new CodexNodeThrowable(this.url, { auth: this.auth }); + + return this._node; + } + + get debug() { + if (this._debug) { + return this._debug; + } + + this._debug = new CodexDebugThrowable(this.url, { auth: this.auth }); + + return this._debug; + } +} diff --git a/src/throwable/throwable.spec.ts b/src/throwable/throwable.spec.ts new file mode 100644 index 0000000..7db62db --- /dev/null +++ b/src/throwable/throwable.spec.ts @@ -0,0 +1,19 @@ +import { assert, describe, it } from "vitest"; +import { CodexDataThrowable } from "../data/data.throwable"; +import { CodexError } from "../errors/errors"; + +describe("data", () => { + const data = new CodexDataThrowable( + process.env["CLIENT_URL"] || "http://localhost:8080" + ); + + it("returns an error when providing an invalid cid", async () => { + try { + await data.delete("hello"); + assert.fail(); + } catch (e) { + assert.ok(e instanceof CodexError); + assert.ok(e.message.includes("Incorrect Cid")); + } + }); +}); diff --git a/src/throwable/throwable.ts b/src/throwable/throwable.ts new file mode 100644 index 0000000..62bcb0d --- /dev/null +++ b/src/throwable/throwable.ts @@ -0,0 +1,13 @@ +import type { SafeValue } from "../values/values"; + +export const Throwable = { + async from(safePromise: Promise>): Promise { + const result = await safePromise; + + if (result.error) { + throw result.data; + } + + return result.data; + }, +};