upend/tools/upend_js/api.ts

283 lines
7.9 KiB
TypeScript
Raw Normal View History

2023-05-21 21:37:29 +02:00
import LRU from "lru-cache";
import { UpListing, UpObject } from ".";
import type {
2023-06-28 18:44:08 +02:00
ADDRESS_TYPE,
2023-05-21 21:37:29 +02:00
Address,
AttributeListingResult,
EntityListing,
IJob,
IValue,
ListingResult,
PutInput,
PutResult,
StoreInfo,
VaultInfo,
} from "./types";
2023-08-27 11:48:44 +02:00
import init_wasm, { InitOutput } from "upend_wasm";
2023-06-28 14:26:11 +02:00
import {
AddressComponents,
AddressTypeConstants,
2023-06-28 14:26:11 +02:00
addr_to_components,
components_to_addr,
} from "upend_wasm";
2023-06-05 12:56:33 +02:00
import debug from "debug";
const dbg = debug("upend:api");
2023-05-21 21:37:29 +02:00
2023-06-28 14:26:11 +02:00
export { AddressComponents };
2023-05-21 21:37:29 +02:00
export class UpEndApi {
private instanceUrl = "";
2023-08-27 11:48:44 +02:00
2023-06-28 14:26:11 +02:00
private wasmPath: string | undefined;
private wasmInitialized = false;
2023-08-27 11:48:44 +02:00
private wasmPromise: Promise<InitOutput> | undefined;
2023-06-28 18:47:00 +02:00
private addressTypeConstants: AddressTypeConstants | undefined = undefined;
2023-05-21 21:37:29 +02:00
private queryOnceLRU = new LRU<string, UpListing>({ max: 128 });
private inFlightRequests: { [key: string]: Promise<UpListing> | null } = {};
2023-06-28 14:26:11 +02:00
constructor(instanceUrl = "", wasmPath?: string) {
2023-05-21 21:37:29 +02:00
this.setInstanceUrl(instanceUrl);
2023-06-28 14:26:11 +02:00
if (wasmPath) {
this.setWasmPath(wasmPath);
}
2023-05-21 21:37:29 +02:00
}
public setInstanceUrl(apiUrl: string) {
this.instanceUrl = apiUrl.replace(/\/+$/g, "");
}
2023-06-28 14:26:11 +02:00
public setWasmPath(wasmPath: string) {
this.wasmPath = wasmPath;
}
public get apiUrl() {
2023-05-21 21:37:29 +02:00
return this.instanceUrl + "/api";
}
public async fetchEntity(address: string): Promise<UpObject> {
2023-06-05 12:56:33 +02:00
dbg("Fetching Entity %s", address);
2023-05-21 21:37:29 +02:00
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) {
2023-06-05 12:56:33 +02:00
dbg("Fetching entry %s", address);
2023-05-21 21:37:29 +02:00
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): Promise<UpListing> {
const cacheResult = this.queryOnceLRU.get(query);
if (!cacheResult) {
if (!this.inFlightRequests[query]) {
2023-06-05 12:56:33 +02:00
dbg(`Querying: ${query}`);
2023-05-21 21:37:29 +02:00
this.inFlightRequests[query] = new Promise((resolve, reject) => {
fetch(`${this.apiUrl}/query`, {
method: "POST",
body: query,
keepalive: true,
})
.then(async (response) => {
resolve(new UpListing(await response.json()));
this.inFlightRequests[query] = null;
})
.catch((err) => reject(err));
});
} else {
2023-06-05 12:56:33 +02:00
dbg(`Chaining request for ${query}...`);
2023-05-21 21:37:29 +02:00
}
return await (this.inFlightRequests[query] as Promise<UpListing>); // TODO?
} else {
2023-06-05 12:56:33 +02:00
dbg(`Returning cached: ${query}`);
2023-05-21 21:37:29 +02:00
return cacheResult;
}
}
public async putEntry(input: PutInput): Promise<PutResult> {
2023-06-05 12:56:33 +02:00
dbg("Putting %O", input);
2023-05-21 21:37:29 +02:00
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,
2023-06-19 16:45:55 +02:00
value: IValue,
provenance?: string
2023-05-21 21:37:29 +02:00
): Promise<Address> {
2023-06-19 16:45:55 +02:00
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, {
2023-05-21 21:37:29 +02:00
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(value),
});
return await response.json();
}
public async putBlob(fileOrUrl: File | URL): Promise<PutResult> {
2023-06-05 12:56:33 +02:00
dbg("Putting Blob: %O", fileOrUrl);
2023-05-21 21:37:29 +02:00
const formData = new FormData();
if (fileOrUrl instanceof File) {
formData.append(fileOrUrl.name, fileOrUrl);
} else {
formData.append("@url", fileOrUrl.toString());
}
2023-05-21 21:37:29 +02:00
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<void> {
2023-06-05 12:56:33 +02:00
dbg("Deleting entry %s", address);
2023-05-21 21:37:29 +02:00
await fetch(`${this.apiUrl}/obj/${address}`, { method: "DELETE" });
}
public async getRaw(address: Address, preview = false) {
2023-06-05 12:56:33 +02:00
dbg("Getting %s raw (preview = %s)", address, preview);
2023-05-21 21:37:29 +02:00
return await fetch(
`${this.apiUrl}/${preview ? "thumb" : "raw"}/${address}`
);
}
public async refreshVault() {
2023-06-05 12:56:33 +02:00
dbg("Triggering vault refresh");
2023-05-21 21:37:29 +02:00
return await fetch(`${this.apiUrl}/refresh`, { method: "POST" });
}
public async nativeOpen(address: Address) {
2023-06-05 12:56:33 +02:00
dbg("Opening %s natively", address);
2023-05-21 21:37:29 +02:00
return fetch(`${this.apiUrl}/raw/${address}?native=1`);
}
public async fetchRoots(): Promise<ListingResult> {
2023-06-05 12:56:33 +02:00
dbg("Fetching hierarchical roots...");
2023-05-21 21:37:29 +02:00
const response = await fetch(`${this.apiUrl}/hier_roots`);
2023-06-05 12:56:33 +02:00
const roots = await response.json();
dbg("Hierarchical roots: %O", roots);
return roots;
2023-05-21 21:37:29 +02:00
}
public async fetchJobs(): Promise<IJob[]> {
const response = await fetch(`${this.apiUrl}/jobs`);
return await response.json();
}
public async fetchAllAttributes(): Promise<AttributeListingResult> {
2023-06-05 12:56:33 +02:00
dbg("Fetching all attributes...");
2023-05-21 21:37:29 +02:00
const response = await fetch(`${this.apiUrl}/all/attributes`);
2023-06-05 12:56:33 +02:00
const result = await response.json();
dbg("All attributes: %O", result);
return await result;
2023-05-21 21:37:29 +02:00
}
public async fetchInfo(): Promise<VaultInfo> {
const response = await fetch(`${this.apiUrl}/info`);
2023-06-05 12:56:33 +02:00
const result = await response.json();
dbg("Vault info: %O", result);
return result;
2023-05-21 21:37:29 +02:00
}
public async fetchStoreInfo(): Promise<{ [key: string]: StoreInfo }> {
2023-06-19 18:51:06 +02:00
const response = await fetch(`${this.apiUrl}/stats/store`);
2023-06-05 12:56:33 +02:00
const result = await response.json();
dbg("Store info: %O");
return await result;
2023-05-21 21:37:29 +02:00
}
public async getAddress(
2023-06-28 14:26:11 +02:00
input:
| { attribute: string }
| { url: string }
| { urlContent: string }
2023-06-28 18:44:08 +02:00
| ADDRESS_TYPE
2023-05-21 21:37:29 +02:00
): Promise<string> {
let response: Response;
2023-06-28 14:26:11 +02:00
if (typeof input === "string") {
try {
2023-06-28 18:47:00 +02:00
if (!this.addressTypeConstants) {
await this.initWasm();
this.addressTypeConstants = new AddressTypeConstants();
}
return this.addressTypeConstants[input];
} catch (err) {
console.warn(err);
}
2023-06-28 14:26:11 +02:00
response = await fetch(`${this.apiUrl}/address?type=${input}`);
2023-05-21 21:37:29 +02:00
} else {
2023-06-28 14:26:11 +02:00
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.");
}
2023-05-21 21:37:29 +02:00
}
2023-06-05 12:56:33 +02:00
const result = await response.json();
dbg("Address for %o = %s", input, result);
return result;
2023-05-21 21:37:29 +02:00
}
2023-06-28 14:26:11 +02:00
public async addressToComponents(
address: string
): Promise<AddressComponents> {
await this.initWasm();
return addr_to_components(address);
}
public async componentsToAddress(
components: AddressComponents
): Promise<string> {
await this.initWasm();
return components_to_addr(components);
}
private async initWasm(): Promise<void> {
if (!this.wasmInitialized) {
if (!this.wasmPath) {
throw new Error(
"Path to WASM file not specified, cannot initialize WASM extensions."
);
}
2023-08-27 11:48:44 +02:00
if (this.wasmPromise) {
await this.wasmPromise;
} else {
dbg("Initializing WASM...");
this.wasmPromise = init_wasm(this.wasmPath);
await this.wasmPromise;
dbg("Wasm initialized.");
}
2023-06-28 14:26:11 +02:00
this.wasmInitialized = true;
}
}
2023-05-21 21:37:29 +02:00
}