diff --git a/README.md b/README.md index 04486e3..1ba5ee7 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ The Codex SDK provides an API for interacting with the Codex decentralized stora The SDK has a small bundle size and support tree shaking. +The SDK is currently under early development and the API can change at any time. + ## Import ```js diff --git a/package-lock.json b/package-lock.json index fd057d2..ef677f3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,7 @@ "typescript": "^5.5.4" }, "engines": { - "node": ">=18" + "node": ">=20" } }, "node_modules/@esbuild/aix-ppc64": { diff --git a/package.json b/package.json index e360a5e..999b507 100644 --- a/package.json +++ b/package.json @@ -7,12 +7,14 @@ "url": "https://github.com/codex-storage/codex-js" }, "scripts": { - "prepack": "npm run build", + "prepack": "tsup tsup src/index.ts --format esm,cjs --dts", "prebuild": "rm -Rf dist/*", - "build": "tsup tsup src/index.ts --format esm,cjs --dts", + "build": "tsc --p tsconfig.test.json", "compile": "tsc --noEmit", "pretest": "npm run build", + "pretest:only": "npm run build", "test": "node --test", + "test:only": "node --test --test-only", "watch": "tsc --watch", "format": "prettier --write ./src" }, @@ -44,7 +46,7 @@ "readme": "README.md", "license": "MIT", "engines": { - "node": ">=18" + "node": ">=20" }, "devDependencies": { "@faker-js/faker": "^8.4.1", diff --git a/src/chunks/chunks.ts b/src/chunks/chunks.ts new file mode 100644 index 0000000..371abda --- /dev/null +++ b/src/chunks/chunks.ts @@ -0,0 +1,19 @@ +export const Chunks = { + async split(file: File) { + const totalSize = file.size; + const chunkSize = 1024 * 1024 * 3; // 10MB + + const chunks = [] as Uint8Array[]; + const amountOfChunks = Math.ceil(totalSize / chunkSize); + + for (let index = 0; index < amountOfChunks; index++) { + const start = index * chunkSize; + const end = (index + 1) * chunkSize; + + const chunk = await file.slice(start, end).arrayBuffer(); + chunks.push(new Uint8Array(chunk)); + } + + return chunks; + }, +}; diff --git a/src/data/data.ts b/src/data/data.ts new file mode 100644 index 0000000..2859da2 --- /dev/null +++ b/src/data/data.ts @@ -0,0 +1,197 @@ +import { Api } from "../api/config"; +import { Chunks } from "../chunks/chunks"; +import { Fetch } from "../fetch-safe/fetch-safe"; +import type { SafeValue } from "../values/values"; +import type { CodexDataResponse, CodexNodeSpace } from "./types"; + +type UploadResponse = { + result: Promise>; + abort: () => void; +}; + +export class Data { + readonly url: string; + + constructor(url: string) { + this.url = url; + } + + /** + * Lists manifest CIDs stored locally in node. + * TODO: remove the faker data part when the api is ready + */ + cids(): Promise> { + const url = this.url + Api.config.prefix + "/data"; + + return Fetch.safeJson(url, { + method: "GET", + }).then((data) => { + if (data.error) { + return data; + } + + /* const mimetypes = [ + "image/png", + "image/jpg", + "image/jpeg", + "audio/mp3", + "video/mp4", + "application/pdf", + "application/msdoc", + "text/plain", + ]; + */ + return { + error: false, + data: { + content: data.data.content /*.map((content) => { + const random = Math.trunc(Math.random() * (mimetypes.length - 1)); + const mimetype = mimetypes[random]; + const [, extension] = mimetype?.split("/") || []; + const filename = Array(5) + .fill("") + .map((_) => ((Math.random() * 36) | 0).toString(36)) + .join(""); + + return { + cid: content.cid, + manifest: { + ...content.manifest, + filename: `${filename}.${extension}`, + mimetype: mimetype || "", + uploadedAt: new Date().toJSON(), + }, + }; + })*/, + }, + }; + }); + } + + /** + * Gets a summary of the storage space allocation of the node. + */ + space() { + const url = this.url + Api.config.prefix + "/space"; + + return Fetch.safeJson(url, { + method: "GET", + }); + } + + /** + * Upload a file in a streaming manner. + * Once completed, the file is stored in the node and can be retrieved by any node in the network using the returned CID. + * XMLHttpRequest is used instead of fetch for this case, to obtain progress information. + * A callback onProgress can be passed to receive upload progress data information. + */ + async upload( + file: File, + onProgress?: (loaded: number, total: number) => void + ): Promise { + const url = this.url + Api.config.prefix + "/data"; + + const xhr = new XMLHttpRequest(); + + const promise = new Promise>(async (resolve) => { + xhr.upload.onprogress = (evt) => { + if (evt.lengthComputable) { + onProgress?.(evt.loaded, evt.total); + } + }; + + xhr.open("POST", url, true); + + await Chunks.split(file); + + xhr.send(new Blob(await Chunks.split(file))); + + xhr.onload = function () { + if (xhr.status != 200) { + resolve({ + error: true, + data: { + code: xhr.status, + message: xhr.responseText, + }, + }); + } else { + resolve({ error: false, data: xhr.response }); + } + }; + + xhr.onerror = function () { + resolve({ + error: true, + data: { + message: "Something went wrong during the file upload.", + }, + }); + }; + }); + + // const promise = Fetch.safe(url, { + // method: "POST", + // headers: { "Content-Type": "text/plain" }, + // body: file.stream(), + // // @ts-ignore + // duplex: "half", + // }) + // .then(async (res) => { + // console.info(res); + // return res.error + // ? res + // : { error: false as false, data: await res.data.text() }; + // }) + + return { + result: promise, + abort: () => { + xhr.abort(); + }, + }; + } + + /** + * Download a file from the local node in a streaming manner. + * If the file is not available locally, a 404 is returned. + * There result is a readable stream. + */ + async localDownload(cid: string) { + const url = this.url + Api.config.prefix + "/data/" + cid; + + const res = await Fetch.safe(url, { + method: "GET", + headers: { + "content-type": "application/octet-stream", + }, + }); + + if (res.error) { + return res; + } + + return res.data.body; + } + + /** + * Download a file from the network in a streaming manner. + * If the file is not available locally, it will be retrieved from other nodes in the network if able. + */ + async networkDownload(cid: string) { + const url = this.url + Api.config.prefix + `/data/${cid}/network`; + + const res = await Fetch.safe(url, { + method: "GET", + headers: { + "content-type": "application/octet-stream", + }, + }); + + if (res.error) { + return res; + } + + return res.data.body; + } +} diff --git a/src/data/types.ts b/src/data/types.ts new file mode 100644 index 0000000..c35edd1 --- /dev/null +++ b/src/data/types.ts @@ -0,0 +1,81 @@ +export type CodexManifest = { + /** + * "Root hash of the content" + */ + // rootHash: string; + + /** + * Length of original content in bytes + */ + // originalBytes: number; + + /** + * Total size of all blocks + */ + datasetSize: number; + + /** + * "Size of blocks" + */ + blockSize: number; + + /** + * Indicates if content is protected by erasure-coding + */ + protected: boolean; + + /** + * Root of the merkle tree + */ + treeCid: string; + + /** + * Name of the name (not implemeted yet) + */ + filename: string; + + /** + * Mimetype (not implemeted yet) + */ + mimetype: string; + + /** + * Date of upload (not implemented at) + */ + uploadedAt: string; +}; + +export type CodexDataContent = { + /** + * Content Identifier as specified at https://github.com/multiformats/cid + */ + cid: string; + + manifest: CodexManifest; +}; + +export type CodexDataResponse = { + content: CodexDataContent[]; +}; + +export type CodexNodeSpace = { + /** + * Number of blocks stored by the node + */ + totalBlocks: number; + + /** + * Maximum storage space used by the node + */ + quotaMaxBytes: number; + + /** + * Amount of storage space currently in use + */ + quotaUsedBytes: number; + + /** + * Amount of storage space reserved + */ + quotaReservedBytes: number; +}; diff --git a/src/debug/debug.test.ts b/src/debug/debug.test.ts new file mode 100644 index 0000000..59b883a --- /dev/null +++ b/src/debug/debug.test.ts @@ -0,0 +1,40 @@ +import assert from "assert"; +import { describe, it } from "node:test"; +import { Fetch } from "../fetch-safe/fetch-safe"; +import { Debug } from "./debug"; +import type { CodexLogLevel } from "./types"; + +describe("debug", () => { + const debug = new Debug("http://localhost:3000"); + + it("returns an error when trying to setup the log level with a bad value", async () => { + const response = await debug.setLogLevel("TEST" as CodexLogLevel); + + assert.deepStrictEqual(response, { + error: true, + data: { + message: "Cannot validate the input", + errors: [ + { + expected: + '"TRACE" | "DEBUG" | "INFO" | "NOTICE" | "WARN" | "ERROR" | "FATAL"', + message: + 'Invalid type: Expected "TRACE" | "DEBUG" | "INFO" | "NOTICE" | "WARN" | "ERROR" | "FATAL" but received "TEST"', + path: undefined, + received: '"TEST"', + }, + ], + }, + }); + }); + + it("returns a success when trying to setup the log level with a correct value", async (t) => { + t.mock.method(Fetch, "safe", () => + Promise.resolve({ error: false, data: true }) + ); + + const response = await debug.setLogLevel("ERROR"); + + assert.deepStrictEqual(response, { error: false, data: true }); + }); +}); diff --git a/src/debug/debug.ts b/src/debug/debug.ts new file mode 100644 index 0000000..405d599 --- /dev/null +++ b/src/debug/debug.ts @@ -0,0 +1,52 @@ +import { Api } from "../api/config"; +import { CodexValibotIssuesMap } from "../errors/errors"; +import { Fetch } from "../fetch-safe/fetch-safe"; +import { CodexLogLevel, type CodexDebugInfo } from "./types"; +import * as v from "valibot"; + +export class Debug { + readonly url: string; + + constructor(url: string) { + this.url = url; + } + + /** + * Set log level at run time + */ + setLogLevel(level: CodexLogLevel) { + const result = v.safeParse(CodexLogLevel, level); + + if (!result.success) { + return { + error: true, + data: { + message: "Cannot validate the input", + errors: CodexValibotIssuesMap(result.issues), + }, + }; + } + + const url = + this.url + + Api.config.prefix + + "/debug/chronicles/loglevel?level=" + + level; + + return Fetch.safe(url, { + method: "POST", + body: "", + }); + } + + /** + * Gets node information + */ + info() { + const url = this.url + Api.config.prefix + `/debug/info`; + + return Fetch.safeJson(url, { + method: "GET", + }); + } +} diff --git a/src/debug/types.ts b/src/debug/types.ts new file mode 100644 index 0000000..140c03a --- /dev/null +++ b/src/debug/types.ts @@ -0,0 +1,33 @@ +import * as v from "valibot"; + +export const CodexLogLevel = v.picklist([ + "TRACE", + "DEBUG", + "INFO", + "NOTICE", + "WARN", + "ERROR", + "FATAL", +]); + +export type CodexLogLevel = v.InferOutput; + +export type CodexDebugInfo = { + /** + * Peer Identity reference as specified at https://docs.libp2p.io/concepts/fundamentals/peers/ + */ + id: string; + + /** + * Address of node as specified by the multi-address specification https://multiformats.io/multiaddr/ + */ + addrs: string[]; + + /** + * Path of the data repository where all nodes data are stored + */ + repo: "string"; + + // Signed Peer Record (libp2p) + spr: string; +}; diff --git a/src/disk/disk.ts b/src/disk/disk.ts deleted file mode 100644 index c3abc8b..0000000 --- a/src/disk/disk.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { SafeValue } from "../values/values"; - -export class Disk { - readonly url: string; - - constructor(url: string) { - this.url = url; - } - - async available(): Promise> { - return { - error: false, - data: { - full: 500, - used: 200, - }, - }; - } -} diff --git a/src/errors/errors.ts b/src/errors/errors.ts index 337f600..d735433 100644 --- a/src/errors/errors.ts +++ b/src/errors/errors.ts @@ -25,5 +25,5 @@ export const CodexValibotIssuesMap = (issues: InferIssue[]) => expected: i.expected, received: i.received, message: i.message, - path: i.path.map((item: { key: string }) => item.key).join("."), + path: i.path?.map((item: { key: string }) => item.key).join("."), })); diff --git a/src/fetch-safe/fetch-safe.test.ts b/src/fetch-safe/fetch-safe.test.ts index 0ec285a..baf7775 100644 --- a/src/fetch-safe/fetch-safe.test.ts +++ b/src/fetch-safe/fetch-safe.test.ts @@ -45,48 +45,45 @@ class MockResponse implements Response { } } -describe("fetch", () => { +describe.only("fetch", () => { it("returns an error when the http call failed", async (t) => { global.fetch = t.mock.fn(() => - Promise.resolve(new MockResponse(false, 500, "error")) + Promise.resolve(new MockResponse(false, 500, "error")), ); - const result = await Fetch.safe("http://localhost:3000/some-url", { + const result = await Fetch.safeJson("http://localhost:3000/some-url", { method: "GET", }); + const error = { - type: "api", message: "error", - status: 500, + code: 500, }; assert.deepStrictEqual(result, { error: true, data: error }); }); - it("returns an error when the json parsing failed", async (t) => { + it.only("returns an error when the json parsing failed", async (t) => { global.fetch = t.mock.fn(() => - Promise.resolve(new MockResponse(true, 200, "")) + Promise.resolve(new MockResponse(true, 200, "")), ); - const result = await Fetch.safe("http://localhost:3000/some-url", { + const result = await Fetch.safeJson("http://localhost:3000/some-url", { method: "GET", }); - const error = { - type: "error", - message: "Unexpected end of JSON input", - }; - assert.deepStrictEqual(result, { error: true, data: error }); + assert.ok(result.error); + assert.deepStrictEqual(result.data.message, "Unexpected end of JSON input"); }); it("returns the data when the fetch succeed", async (t) => { global.fetch = t.mock.fn(() => Promise.resolve( - new MockResponse(true, 200, JSON.stringify({ hello: "world" })) - ) + new MockResponse(true, 200, JSON.stringify({ hello: "world" })), + ), ); - const result = await Fetch.safe("http://localhost:3000/some-url", { + const result = await Fetch.safeJson("http://localhost:3000/some-url", { method: "GET", }); diff --git a/src/fetch-safe/fetch-safe.ts b/src/fetch-safe/fetch-safe.ts index 649efec..dbd8e48 100644 --- a/src/fetch-safe/fetch-safe.ts +++ b/src/fetch-safe/fetch-safe.ts @@ -1,37 +1,50 @@ +import { Promises } from "../promise-safe/promise-safe"; import { type SafeValue } from "../values/values"; export const Fetch = { - async safe( + async safe(url: string, init: RequestInit): Promise> { + const res = await Promises.safe(() => fetch(url, init)); + + if (res.error) { + return { + error: true, + data: { + message: + "The connection with the Codex node seems to be broken. Please check your node is running.", + code: 502, + }, + }; + } + + if (!res.data.ok) { + const message = await Promises.safe(() => res.data.text()); + + if (message.error) { + return message; + } + + return { + error: true, + data: { + message: message.data, + code: res.data.status, + }, + }; + } + + return { error: false, data: res.data }; + }, + + async safeJson( url: string, - init: RequestInit, + init: RequestInit ): Promise> { - const res = await fetch(url, init); + const res = await this.safe(url, init); - if (!res.ok) { - const message = await res.text(); - - return { - error: true, - data: { - message, - code: res.status, - }, - }; + if (res.error) { + return res; } - try { - const json = await res.json(); - - return { error: false, data: json }; - } catch (e) { - const opts = e instanceof Error && e.stack ? { stack: e.stack } : {}; - return { - error: true, - data: { - message: e instanceof Error ? e.message : "JSON parsing error :" + e, - ...opts, - }, - }; - } + return Promises.safe(() => res.data.json()); }, }; diff --git a/src/index.ts b/src/index.ts index 2a931b2..4e4e902 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,18 +1,28 @@ -import { Disk } from "./disk/disk"; +import type { Data } from "./data/data"; +import type { Node } from "./node/node"; import { Marketplace } from "./marketplace/marketplace"; +import type { Debug } from "./debug/debug"; 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 class Codex { readonly url: string; private _marketplace: Marketplace | null; - readonly disk: Disk; + private _data: Data | null; + private _node: Node | null; + private _debug: Debug | null; constructor(url: string) { this.url = url; this._marketplace = null; - this.disk = new Disk(url); + this._data = null; + this._node = null; + this._debug = null; } async marketplace() { @@ -24,6 +34,42 @@ export class Codex { this._marketplace = new module.Marketplace(this.url); - return module.Marketplace; + return this._marketplace; + } + + async data() { + if (this._data) { + return this._data; + } + + const module = await import("./data/data"); + + this._data = new module.Data(this.url); + + return this._data; + } + + async node() { + if (this._node) { + return this._node; + } + + const module = await import("./node/node"); + + this._node = new module.Node(this.url); + + return this._node; + } + + async debug() { + if (this._debug) { + return this._debug; + } + + const module = await import("./debug/debug"); + + this._debug = new module.Debug(this.url); + + return this._debug; } } diff --git a/src/marketplace/marketplace.test.ts b/src/marketplace/marketplace.test.ts index 23e8d54..59281f8 100644 --- a/src/marketplace/marketplace.test.ts +++ b/src/marketplace/marketplace.test.ts @@ -5,34 +5,34 @@ import { Fetch } from "../fetch-safe/fetch-safe"; import { Marketplace } from "./marketplace"; // function createSlot() { -// return { -// "id": faker.string.alphanumeric(64), -// "request": { +// return { +// "id": faker.string.alphanumeric(64), +// "request": { -// "id": faker.string.alphanumeric(64), -// "client": faker.finance.ethereumAddress(), -// "ask": -// { -// "slots": faker.number.int({ min: 0, max: 9 }), -// "slotSize": faker.number.float({ max: 10000 }).toString(), -// "duration": faker.number.int({ max: 300000 }).toString(), -// "proofProbability": faker.number.int({ max: 9 }), -// "reward": faker.number.float({ max: 1000 }).toString(), -// "maxSlotLoss": faker.number.int({ max: 9 }) -// }, -// "content": { -// "cid": faker.string.alphanumeric(64), -// "por": { -// "u": faker.string.alphanumeric(16), -// "publicKey": faker.string.alphanumeric(64), -// "name": faker.string.alphanumeric(16) -// } -// }, -// "expiry": faker.number.int({ min: 2, max: 59 }) + " minutes", -// "nonce": faker.string.alphanumeric(64) -// }, -// "slotIndex": faker.number.int({ min: 0, max: 9 }) -// } +// "id": faker.string.alphanumeric(64), +// "client": faker.finance.ethereumAddress(), +// "ask": +// { +// "slots": faker.number.int({ min: 0, max: 9 }), +// "slotSize": faker.number.float({ max: 10000 }).toString(), +// "duration": faker.number.int({ max: 300000 }).toString(), +// "proofProbability": faker.number.int({ max: 9 }), +// "reward": faker.number.float({ max: 1000 }).toString(), +// "maxSlotLoss": faker.number.int({ max: 9 }) +// }, +// "content": { +// "cid": faker.string.alphanumeric(64), +// "por": { +// "u": faker.string.alphanumeric(16), +// "publicKey": faker.string.alphanumeric(64), +// "name": faker.string.alphanumeric(16) +// } +// }, +// "expiry": faker.number.int({ min: 2, max: 59 }) + " minutes", +// "nonce": faker.string.alphanumeric(64) +// }, +// "slotIndex": faker.number.int({ min: 0, max: 9 }) +// } // } function createStorageRequest() { @@ -52,7 +52,6 @@ function missingNumberValidationError(field: string) { return { error: true, data: { - type: "validation", message: "Cannot validate the input", errors: [ { @@ -70,7 +69,6 @@ function extraValidationError(field: string, value: unknown) { return { error: true, data: { - type: "validation", message: "Cannot validate the input", errors: [ { @@ -88,7 +86,6 @@ function missingStringValidationError(field: string) { return { error: true, data: { - type: "validation", message: "Cannot validate the input", errors: [ { @@ -106,7 +103,6 @@ function mistypeNumberValidationError(field: string, value: string) { return { error: true, data: { - type: "validation", message: "Cannot validate the input", errors: [ { @@ -124,7 +120,6 @@ function minNumberValidationError(field: string, min: number) { return { error: true, data: { - type: "validation", message: "Cannot validate the input", errors: [ { @@ -161,7 +156,7 @@ describe("marketplace", () => { assert.deepStrictEqual(response, missingNumberValidationError("totalSize")); }); - it.only("returns an error when trying to create an availability with an invalid number valid", async () => { + it("returns an error when trying to create an availability with an invalid number valid", async () => { const response = await marketplace.createAvailability({ duration: 3000, maxCollateral: 1, @@ -193,8 +188,6 @@ describe("marketplace", () => { minPrice: 100, } as any); - console.info(response.error); - assert.deepStrictEqual(response, missingNumberValidationError("duration")); }); @@ -247,7 +240,9 @@ describe("marketplace", () => { it("returns a response when the request succeed", async (t) => { const data = { ...createAvailability(), freeSize: 1000 }; - t.mock.method(Fetch, "safe", () => Promise.resolve({ error: false, data })); + t.mock.method(Fetch, "safeJson", () => + Promise.resolve({ error: false, data }) + ); const response = await marketplace.createAvailability({ maxCollateral: 1, @@ -262,7 +257,9 @@ describe("marketplace", () => { it("returns a response when the create availability succeed", async (t) => { const data = { ...createAvailability(), freeSize: 1000 }; - t.mock.method(Fetch, "safe", () => Promise.resolve({ error: false, data })); + t.mock.method(Fetch, "safeJson", () => + Promise.resolve({ error: false, data }) + ); const response = await marketplace.createAvailability({ maxCollateral: 1, @@ -301,7 +298,9 @@ describe("marketplace", () => { it("returns a response when the update availability succeed", async (t) => { const data = createAvailability(); - t.mock.method(Fetch, "safe", () => Promise.resolve({ error: false, data })); + t.mock.method(Fetch, "safeJson", () => + Promise.resolve({ error: false, data }) + ); const response = await marketplace.updateAvailability({ id: faker.string.alphanumeric(64), diff --git a/src/marketplace/marketplace.ts b/src/marketplace/marketplace.ts index 98b2692..2e8a70f 100644 --- a/src/marketplace/marketplace.ts +++ b/src/marketplace/marketplace.ts @@ -28,7 +28,7 @@ export class Marketplace { async activeSlots(): Promise> { const url = this.url + Api.config.prefix + "/sales/slots"; - return Fetch.safe(url, { + return Fetch.safeJson(url, { method: "GET", }); } @@ -39,7 +39,7 @@ export class Marketplace { async activeSlot(slotId: string): Promise> { const url = this.url + Api.config.prefix + "/sales/slots/" + slotId; - return Fetch.safe(url, { + return Fetch.safeJson(url, { method: "GET", }); } @@ -50,7 +50,7 @@ export class Marketplace { async availabilities(): Promise> { const url = this.url + Api.config.prefix + "/sales/availability"; - return Fetch.safe(url, { + return Fetch.safeJson(url, { method: "GET", }); } @@ -75,7 +75,7 @@ export class Marketplace { const url = this.url + Api.config.prefix + "/sales/availability"; - return Fetch.safe(url, { + return Fetch.safeJson(url, { method: "POST", headers: { "content-type": "application/json", @@ -106,7 +106,7 @@ export class Marketplace { const url = this.url + Api.config.prefix + "/sales/availability/" + result.output.id; - return Fetch.safe(url, { + return Fetch.safeJson(url, { method: "POST", headers: { "content-type": "application/json", @@ -126,7 +126,7 @@ export class Marketplace { Api.config.prefix + `/sales/availability/${availabilityId}/reservations`; - return Fetch.safe(url, { + return Fetch.safeJson(url, { method: "GET", }); } @@ -137,7 +137,7 @@ export class Marketplace { async purchaseIds(): Promise> { const url = this.url + Api.config.prefix + `/storage/purchases`; - return Fetch.safe(url, { + return Fetch.safeJson(url, { method: "GET", }); } @@ -149,7 +149,7 @@ export class Marketplace { const url = this.url + Api.config.prefix + `/storage/purchases/` + purchaseId; - return Fetch.safe(url, { + return Fetch.safeJson(url, { method: "GET", }); } @@ -175,7 +175,7 @@ export class Marketplace { const { cid, ...body } = result.output; const url = this.url + Api.config.prefix + "/storage/request/" + cid; - return Fetch.safe(url, { + return Fetch.safeJson(url, { method: "POST", headers: { "content-type": "application/json", diff --git a/src/node/node.ts b/src/node/node.ts new file mode 100644 index 0000000..e234c2f --- /dev/null +++ b/src/node/node.ts @@ -0,0 +1,53 @@ +import { Api } from "../api/config"; +import { Fetch } from "../fetch-safe/fetch-safe"; + +export class Node { + readonly url: string; + + constructor(url: string) { + this.url = url; + } + + /** + * Connect to a peer + * TODO check result + */ + connect(peerId: string, addrs: string[] = []) { + const params = new URLSearchParams(); + + for (const addr of addrs) { + params.append("addrs", addr); + } + + const url = + this.url + Api.config.prefix + `/connect/${peerId}?` + addrs.toString(); + + return Fetch.safe(url, { + method: "GET", + }); + } + + /** + * Get Node's SPR + * TODO check result + */ + spr() { + const url = this.url + Api.config.prefix + "/spr"; + + return Fetch.safe(url, { + method: "GET", + }); + } + + /** + * Get Node's PeerID + * TODO check result + */ + peerId() { + const url = this.url + Api.config.prefix + "/node/peerid"; + + return Fetch.safe(url, { + method: "GET", + }); + } +} diff --git a/src/promise-safe/promise-safe.test.ts b/src/promise-safe/promise-safe.test.ts new file mode 100644 index 0000000..bb5738f --- /dev/null +++ b/src/promise-safe/promise-safe.test.ts @@ -0,0 +1,21 @@ +import assert from "assert"; +import { describe, it } from "node:test"; +import { Promises } from "./promise-safe"; + +describe("promise safe", () => { + it("returns an error when the promise failed", async () => { + const result = await Promises.safe( + () => new Promise((_, reject) => reject("error")), + ); + + assert.deepStrictEqual(result, { error: true, data: { message: "error" } }); + }); + + it("returns the value when the promise succeed", async () => { + const result = await Promises.safe( + () => new Promise((resolve) => resolve("ok")), + ); + + assert.deepStrictEqual(result, { error: false, data: "ok" }); + }); +}); diff --git a/src/promise-safe/promise-safe.ts b/src/promise-safe/promise-safe.ts new file mode 100644 index 0000000..95065c5 --- /dev/null +++ b/src/promise-safe/promise-safe.ts @@ -0,0 +1,21 @@ +import type { SafeValue } from "../values/values"; + +export const Promises = { + async safe(promise: () => Promise): Promise> { + try { + const result = await promise(); + + return { error: false, data: result }; + } catch (e) { + const opts = e instanceof Error && e.stack ? { stack: e.stack } : {}; + + return { + error: true, + data: { + message: e instanceof Error ? e.message : "" + e, + ...opts, + }, + }; + } + }, +}; diff --git a/tsconfig.json b/tsconfig.json index a4ca748..63a344c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,7 +13,6 @@ "outDir": "./dist", "module": "ESNext", "moduleResolution": "Bundler", - "verbatimModuleSyntax": true, - }, - "buildOptions": {} + "verbatimModuleSyntax": true + } } \ No newline at end of file diff --git a/tsconfig.test.json b/tsconfig.test.json new file mode 100644 index 0000000..4c5e498 --- /dev/null +++ b/tsconfig.test.json @@ -0,0 +1,14 @@ +{ + "extends": "@tsconfig/strictest/tsconfig.json", + "include": [ + "src" + ], + "compilerOptions": { + "declaration": true, + "lib": [ + "ES2020", + "DOM", + ], + "outDir": "./dist" + } +} \ No newline at end of file