273 lines
7.7 KiB
TypeScript
273 lines
7.7 KiB
TypeScript
import LRU from "lru-cache";
|
|
import { UpListing, UpObject } from ".";
|
|
import type {
|
|
ADDRESS_TYPE,
|
|
Address,
|
|
AttributeListingResult,
|
|
EntityListing,
|
|
IJob,
|
|
IValue,
|
|
ListingResult,
|
|
PutInput,
|
|
PutResult,
|
|
StoreInfo,
|
|
VaultInfo,
|
|
} from "./types";
|
|
import init_wasm from "upend_wasm";
|
|
import {
|
|
AddressComponents,
|
|
AddressTypeConstants,
|
|
addr_to_components,
|
|
components_to_addr,
|
|
InitInput,
|
|
} from "upend_wasm";
|
|
import debug from "debug";
|
|
const dbg = debug("upend:api");
|
|
|
|
export { AddressComponents };
|
|
|
|
export class UpEndApi {
|
|
private instanceUrl = "";
|
|
private wasmInit: InitInput | undefined;
|
|
private wasmInitialized = false;
|
|
private addressTypeConstants: AddressTypeConstants | undefined = undefined;
|
|
|
|
private queryOnceLRU = new LRU<string, UpListing>({ max: 128 });
|
|
private inFlightRequests: { [key: string]: Promise<UpListing> | null } = {};
|
|
|
|
constructor(instanceUrl = "", wasmInit?: InitInput) {
|
|
this.setInstanceUrl(instanceUrl);
|
|
if (wasmInit) {
|
|
this.setWasmInit(wasmInit);
|
|
}
|
|
}
|
|
|
|
public setInstanceUrl(apiUrl: string) {
|
|
this.instanceUrl = apiUrl.replace(/\/+$/g, "");
|
|
}
|
|
|
|
public setWasmInit(wasmInit: InitInput) {
|
|
this.wasmInit = wasmInit;
|
|
}
|
|
|
|
public get apiUrl() {
|
|
return this.instanceUrl + "/api";
|
|
}
|
|
|
|
public async fetchEntity(address: string): Promise<UpObject> {
|
|
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): Promise<UpListing> {
|
|
const cacheResult = this.queryOnceLRU.get(query);
|
|
if (!cacheResult) {
|
|
if (!this.inFlightRequests[query]) {
|
|
dbg(`Querying: ${query}`);
|
|
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 {
|
|
dbg(`Chaining request for ${query}...`);
|
|
}
|
|
return await (this.inFlightRequests[query] as Promise<UpListing>); // TODO?
|
|
} else {
|
|
dbg(`Returning cached: ${query}`);
|
|
return cacheResult;
|
|
}
|
|
}
|
|
|
|
public async putEntry(input: PutInput): Promise<PutResult> {
|
|
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<Address> {
|
|
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<PutResult> {
|
|
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<void> {
|
|
dbg("Deleting entry %s", address);
|
|
await fetch(`${this.apiUrl}/obj/${address}`, { method: "DELETE" });
|
|
}
|
|
|
|
public async getRaw(address: Address, preview = false) {
|
|
dbg("Getting %s raw (preview = %s)", address, preview);
|
|
return await fetch(
|
|
`${this.apiUrl}/${preview ? "thumb" : "raw"}/${address}`
|
|
);
|
|
}
|
|
|
|
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<ListingResult> {
|
|
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<IJob[]> {
|
|
const response = await fetch(`${this.apiUrl}/jobs`);
|
|
return await response.json();
|
|
}
|
|
|
|
public async fetchAllAttributes(): Promise<AttributeListingResult> {
|
|
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<VaultInfo> {
|
|
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<string> {
|
|
let response: Response;
|
|
if (typeof input === "string") {
|
|
try {
|
|
if (!this.addressTypeConstants) {
|
|
await this.initWasm();
|
|
this.addressTypeConstants = new AddressTypeConstants();
|
|
}
|
|
return this.addressTypeConstants[input];
|
|
} catch (err) {
|
|
console.warn(err);
|
|
}
|
|
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<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.wasmInit) {
|
|
throw new Error(
|
|
"WASM init not specified, cannot initialize WASM extensions."
|
|
);
|
|
}
|
|
await init_wasm(this.wasmInit);
|
|
this.wasmInitialized = true;
|
|
}
|
|
}
|
|
}
|