import LRU from "lru-cache"; import type { Query, UpObject } from "."; import { UpListing } from "."; import type { ADDRESS_TYPE, Address, AttributeListingResult, EntityListing, IJob, IValue, ListingResult, PutInput, PutResult, StoreInfo, VaultInfo, } from "./types"; import type { UpEndWasmExtensions, AddressComponents } from "./wasm"; import debug from "debug"; const dbg = debug("upend:api"); export type { AddressComponents }; export class UpEndApi { private instanceUrl = ""; private wasmExtensions: UpEndWasmExtensions | undefined = undefined; private queryOnceLRU = new LRU({ max: 128 }); private inFlightRequests: { [key: string]: Promise | null } = {}; constructor(config: { instanceUrl?: string; wasmExtensions?: UpEndWasmExtensions; }) { this.setInstanceUrl(config.instanceUrl || "http://localhost:8093"); this.wasmExtensions = config.wasmExtensions; } public setInstanceUrl(apiUrl: string) { this.instanceUrl = apiUrl.replace(/\/+$/g, ""); } public get apiUrl() { return this.instanceUrl + "/api"; } public async fetchEntity(address: string): Promise { dbg("Fetching Entity %s", address); const entityFetch = await fetch(`${this.apiUrl}/obj/${address}`); const entityResult = (await entityFetch.json()) as EntityListing; const entityListing = new UpListing(entityResult.entries); return entityListing.getObject(address); } public async fetchEntry(address: string) { dbg("Fetching entry %s", address); const response = await fetch(`${this.apiUrl}/raw/${address}`); const data = await response.json(); const listing = new UpListing({ address: data }); return listing.entries[0]; } public async query(query: string | Query): Promise { const queryStr = query.toString(); const cacheResult = this.queryOnceLRU.get(queryStr); if (!cacheResult) { if (!this.inFlightRequests[queryStr]) { dbg(`Querying: ${query}`); this.inFlightRequests[queryStr] = new Promise((resolve, reject) => { fetch(`${this.apiUrl}/query`, { method: "POST", body: queryStr, keepalive: true, }) .then(async (response) => { if (!response.ok) { reject( `Query ${queryStr} failed: ${response.status} ${ response.statusText }: ${await response.text()}}` ); } resolve(new UpListing(await response.json())); this.inFlightRequests[queryStr] = null; }) .catch((err) => reject(err)); }); } else { dbg(`Chaining request for ${queryStr}...`); } return await (this.inFlightRequests[queryStr] as Promise); // TODO? } else { dbg(`Returning cached: ${queryStr}`); return cacheResult; } } public async putEntry(input: PutInput): Promise { dbg("Putting %O", input); const response = await fetch(`${this.apiUrl}/obj`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(input), }); return await response.json(); } public async putEntityAttribute( entity: Address, attribute: string, value: IValue, provenance?: string ): Promise
{ dbg("Putting %s = %o for %s (%s)", attribute, value, entity, provenance); let url = `${this.apiUrl}/obj/${entity}/${attribute}`; if (provenance) { url += `?provenance=${provenance}`; } const response = await fetch(url, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(value), }); return await response.json(); } public async putBlob(fileOrUrl: File | URL): Promise { dbg("Putting Blob: %O", fileOrUrl); const formData = new FormData(); if (fileOrUrl instanceof File) { formData.append(fileOrUrl.name, fileOrUrl); } else { formData.append("@url", fileOrUrl.toString()); } const response = await fetch(`${this.apiUrl}/blob`, { method: "PUT", body: formData, }); if (!response.ok) { throw Error(await response.text()); } return await response.json(); } public async deleteEntry(address: Address): Promise { dbg("Deleting entry %s", address); await fetch(`${this.apiUrl}/obj/${address}`, { method: "DELETE" }); } public getRaw(address: Address, preview = false) { return `${this.apiUrl}/${preview ? "thumb" : "raw"}/${address}`; } public async fetchRaw(address: Address, preview = false) { dbg("Getting %s raw (preview = %s)", address, preview); return await fetch(this.getRaw(address, preview)); } public async refreshVault() { dbg("Triggering vault refresh"); return await fetch(`${this.apiUrl}/refresh`, { method: "POST" }); } public async nativeOpen(address: Address) { dbg("Opening %s natively", address); return fetch(`${this.apiUrl}/raw/${address}?native=1`); } public async fetchRoots(): Promise { dbg("Fetching hierarchical roots..."); const response = await fetch(`${this.apiUrl}/hier_roots`); const roots = await response.json(); dbg("Hierarchical roots: %O", roots); return roots; } public async fetchJobs(): Promise { const response = await fetch(`${this.apiUrl}/jobs`); return await response.json(); } public async fetchAllAttributes(): Promise { dbg("Fetching all attributes..."); const response = await fetch(`${this.apiUrl}/all/attributes`); const result = await response.json(); dbg("All attributes: %O", result); return await result; } public async fetchInfo(): Promise { const response = await fetch(`${this.apiUrl}/info`); const result = await response.json(); dbg("Vault info: %O", result); return result; } public async fetchStoreInfo(): Promise<{ [key: string]: StoreInfo }> { const response = await fetch(`${this.apiUrl}/stats/store`); const result = await response.json(); dbg("Store info: %O"); return await result; } public async getAddress( input: | { attribute: string } | { url: string } | { urlContent: string } | ADDRESS_TYPE ): Promise { let response: Response; if (typeof input === "string") { if (this.wasmExtensions) { await this.wasmExtensions.init(); return this.wasmExtensions.AddressTypeConstants[input]; } response = await fetch(`${this.apiUrl}/address?type=${input}`); } else { if ("attribute" in input) { response = await fetch( `${this.apiUrl}/address?attribute=${input.attribute}` ); } else if ("url" in input) { response = await fetch(`${this.apiUrl}/address?url=${input.url}`); } else if ("urlContent" in input) { response = await fetch( `${this.apiUrl}/address?url_content=${input.urlContent}` ); } else { throw new Error("Input cannot be empty."); } } const result = await response.json(); dbg("Address for %o = %s", input, result); return result; } public async addressToComponents( address: string ): Promise { if (!this.wasmExtensions) { throw new Error("WASM extensions not supplied."); } await this.wasmExtensions.init(); return this.wasmExtensions.addr_to_components(address); } public async componentsToAddress( components: AddressComponents ): Promise { if (!this.wasmExtensions) { throw new Error("WASM extensions not initialized."); } await this.wasmExtensions.init(); return this.wasmExtensions.components_to_addr(components); } }