parent
309a968550
commit
2958d44cc0
|
@ -22,7 +22,8 @@ export type { AddressComponents };
|
|||
|
||||
export class UpEndApi {
|
||||
private instanceUrl = "";
|
||||
private wasmExtensions: UpEndWasmExtensions | undefined = undefined;
|
||||
private readonly wasmExtensions: UpEndWasmExtensions | undefined = undefined;
|
||||
public readonly timeout: number;
|
||||
|
||||
private queryOnceLRU = new LRU<string, UpListing>({ max: 128 });
|
||||
private inFlightRequests: { [key: string]: Promise<UpListing> | null } = {};
|
||||
|
@ -30,9 +31,11 @@ export class UpEndApi {
|
|||
constructor(config: {
|
||||
instanceUrl?: string;
|
||||
wasmExtensions?: UpEndWasmExtensions;
|
||||
timeout?: number;
|
||||
}) {
|
||||
this.setInstanceUrl(config.instanceUrl || "http://localhost:8093");
|
||||
this.wasmExtensions = config.wasmExtensions;
|
||||
this.timeout = config.timeout || 30_000;
|
||||
}
|
||||
|
||||
public setInstanceUrl(apiUrl: string) {
|
||||
|
@ -43,23 +46,33 @@ export class UpEndApi {
|
|||
return this.instanceUrl + "/api";
|
||||
}
|
||||
|
||||
public async fetchEntity(address: string): Promise<UpObject> {
|
||||
public async fetchEntity(
|
||||
address: string,
|
||||
options?: ApiFetchOptions,
|
||||
): Promise<UpObject> {
|
||||
dbg("Fetching Entity %s", address);
|
||||
const entityFetch = await fetch(`${this.apiUrl}/obj/${address}`);
|
||||
const signal = this.getAbortSignal(options);
|
||||
const entityFetch = await fetch(`${this.apiUrl}/obj/${address}`, {
|
||||
signal,
|
||||
});
|
||||
const entityResult = (await entityFetch.json()) as EntityListing;
|
||||
const entityListing = new UpListing(entityResult.entries);
|
||||
return entityListing.getObject(address);
|
||||
}
|
||||
|
||||
public async fetchEntry(address: string) {
|
||||
public async fetchEntry(address: string, options?: ApiFetchOptions) {
|
||||
dbg("Fetching entry %s", address);
|
||||
const response = await fetch(`${this.apiUrl}/raw/${address}`);
|
||||
const signal = this.getAbortSignal(options);
|
||||
const response = await fetch(`${this.apiUrl}/raw/${address}`, { signal });
|
||||
const data = await response.json();
|
||||
const listing = new UpListing({ address: data });
|
||||
return listing.entries[0];
|
||||
}
|
||||
|
||||
public async query(query: string | Query): Promise<UpListing> {
|
||||
public async query(
|
||||
query: string | Query,
|
||||
options?: ApiFetchOptions,
|
||||
): Promise<UpListing> {
|
||||
const queryStr = query.toString();
|
||||
|
||||
const cacheResult = this.queryOnceLRU.get(queryStr);
|
||||
|
@ -67,17 +80,19 @@ export class UpEndApi {
|
|||
if (!this.inFlightRequests[queryStr]) {
|
||||
dbg(`Querying: ${query}`);
|
||||
this.inFlightRequests[queryStr] = new Promise((resolve, reject) => {
|
||||
const signal = this.getAbortSignal(options);
|
||||
fetch(`${this.apiUrl}/query`, {
|
||||
method: "POST",
|
||||
body: queryStr,
|
||||
keepalive: true,
|
||||
signal,
|
||||
})
|
||||
.then(async (response) => {
|
||||
if (!response.ok) {
|
||||
reject(
|
||||
`Query ${queryStr} failed: ${response.status} ${
|
||||
response.statusText
|
||||
}: ${await response.text()}}`
|
||||
}: ${await response.text()}}`,
|
||||
);
|
||||
}
|
||||
resolve(new UpListing(await response.json()));
|
||||
|
@ -95,12 +110,17 @@ export class UpEndApi {
|
|||
}
|
||||
}
|
||||
|
||||
public async putEntry(input: PutInput): Promise<PutResult> {
|
||||
public async putEntry(
|
||||
input: PutInput,
|
||||
options?: ApiFetchOptions,
|
||||
): Promise<PutResult> {
|
||||
dbg("Putting %O", input);
|
||||
const signal = this.getAbortSignal(options);
|
||||
const response = await fetch(`${this.apiUrl}/obj`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(input),
|
||||
signal,
|
||||
});
|
||||
|
||||
return await response.json();
|
||||
|
@ -110,23 +130,30 @@ export class UpEndApi {
|
|||
entity: Address,
|
||||
attribute: string,
|
||||
value: IValue,
|
||||
provenance?: string
|
||||
provenance?: string,
|
||||
options?: ApiFetchOptions,
|
||||
): 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 signal = this.getAbortSignal(options);
|
||||
const response = await fetch(url, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(value),
|
||||
signal,
|
||||
});
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
public async putBlob(fileOrUrl: File | URL): Promise<PutResult> {
|
||||
public async putBlob(
|
||||
fileOrUrl: File | URL,
|
||||
options?: ApiFetchOptions,
|
||||
): Promise<PutResult> {
|
||||
dbg("Putting Blob: %O", fileOrUrl);
|
||||
const formData = new FormData();
|
||||
if (fileOrUrl instanceof File) {
|
||||
|
@ -135,9 +162,11 @@ export class UpEndApi {
|
|||
formData.append("@url", fileOrUrl.toString());
|
||||
}
|
||||
|
||||
const signal = this.getAbortSignal(options);
|
||||
const response = await fetch(`${this.apiUrl}/blob`, {
|
||||
method: "PUT",
|
||||
body: formData,
|
||||
signal,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
|
@ -147,67 +176,91 @@ export class UpEndApi {
|
|||
return await response.json();
|
||||
}
|
||||
|
||||
public async deleteEntry(address: Address): Promise<void> {
|
||||
public async deleteEntry(
|
||||
address: Address,
|
||||
options?: ApiFetchOptions,
|
||||
): Promise<void> {
|
||||
dbg("Deleting entry %s", address);
|
||||
await fetch(`${this.apiUrl}/obj/${address}`, { method: "DELETE" });
|
||||
const signal = this.getAbortSignal(options);
|
||||
await fetch(`${this.apiUrl}/obj/${address}`, { method: "DELETE", signal });
|
||||
}
|
||||
|
||||
public getRaw(address: Address, preview = false) {
|
||||
return `${this.apiUrl}/${preview ? "thumb" : "raw"}/${address}`;
|
||||
}
|
||||
|
||||
public async fetchRaw(address: Address, preview = false) {
|
||||
public async fetchRaw(
|
||||
address: Address,
|
||||
preview = false,
|
||||
options?: ApiFetchOptions,
|
||||
) {
|
||||
dbg("Getting %s raw (preview = %s)", address, preview);
|
||||
return await fetch(this.getRaw(address, preview));
|
||||
const signal = this.getAbortSignal(options);
|
||||
return await fetch(this.getRaw(address, preview), { signal });
|
||||
}
|
||||
|
||||
public async refreshVault() {
|
||||
public async refreshVault(options?: ApiFetchOptions) {
|
||||
dbg("Triggering vault refresh");
|
||||
return await fetch(`${this.apiUrl}/refresh`, { method: "POST" });
|
||||
const signal = this.getAbortSignal(options);
|
||||
return await fetch(`${this.apiUrl}/refresh`, { method: "POST", signal });
|
||||
}
|
||||
|
||||
public async nativeOpen(address: Address) {
|
||||
public async nativeOpen(address: Address, options?: ApiFetchOptions) {
|
||||
dbg("Opening %s natively", address);
|
||||
return fetch(`${this.apiUrl}/raw/${address}?native=1`);
|
||||
const signal = this.getAbortSignal(options);
|
||||
return fetch(`${this.apiUrl}/raw/${address}?native=1`, { signal });
|
||||
}
|
||||
|
||||
public async fetchRoots(): Promise<ListingResult> {
|
||||
public async fetchRoots(options?: ApiFetchOptions): Promise<ListingResult> {
|
||||
dbg("Fetching hierarchical roots...");
|
||||
const response = await fetch(`${this.apiUrl}/hier_roots`);
|
||||
const signal = this.getAbortSignal(options);
|
||||
const response = await fetch(`${this.apiUrl}/hier_roots`, { signal });
|
||||
const roots = await response.json();
|
||||
dbg("Hierarchical roots: %O", roots);
|
||||
return roots;
|
||||
}
|
||||
|
||||
public async fetchJobs(): Promise<IJob[]> {
|
||||
const response = await fetch(`${this.apiUrl}/jobs`);
|
||||
public async fetchJobs(options?: ApiFetchOptions): Promise<IJob[]> {
|
||||
// dbg("Fetching jobs...");
|
||||
const signal = this.getAbortSignal(options);
|
||||
const response = await fetch(`${this.apiUrl}/jobs`, { signal });
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
public async fetchAllAttributes(): Promise<AttributeListingResult> {
|
||||
public async fetchAllAttributes(
|
||||
options?: ApiFetchOptions,
|
||||
): Promise<AttributeListingResult> {
|
||||
dbg("Fetching all attributes...");
|
||||
const response = await fetch(`${this.apiUrl}/all/attributes`);
|
||||
const signal = this.getAbortSignal(options);
|
||||
const response = await fetch(`${this.apiUrl}/all/attributes`, { signal });
|
||||
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`);
|
||||
public async fetchInfo(options?: ApiFetchOptions): Promise<VaultInfo> {
|
||||
dbg("Fetching vault info...");
|
||||
const signal = this.getAbortSignal(options);
|
||||
const response = await fetch(`${this.apiUrl}/info`, { signal });
|
||||
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`);
|
||||
public async fetchStoreInfo(
|
||||
options?: ApiFetchOptions,
|
||||
): Promise<{ [key: string]: StoreInfo }> {
|
||||
dbg("Fetching store info...");
|
||||
const signal = this.getAbortSignal(options);
|
||||
const response = await fetch(`${this.apiUrl}/stats/store`, { signal });
|
||||
const result = await response.json();
|
||||
dbg("Store info: %O");
|
||||
return await result;
|
||||
}
|
||||
|
||||
public async getAddress(
|
||||
input: { urlContent: string } | ADDRESS_TYPE
|
||||
input: { urlContent: string } | ADDRESS_TYPE,
|
||||
options?: ApiFetchOptions,
|
||||
): Promise<string> {
|
||||
let response: Response;
|
||||
if (typeof input === "string") {
|
||||
|
@ -215,11 +268,16 @@ export class UpEndApi {
|
|||
await this.wasmExtensions.init();
|
||||
return this.wasmExtensions.AddressTypeConstants[input];
|
||||
}
|
||||
response = await fetch(`${this.apiUrl}/address?type=${input}`);
|
||||
const signal = this.getAbortSignal(options);
|
||||
response = await fetch(`${this.apiUrl}/address?type=${input}`, {
|
||||
signal,
|
||||
});
|
||||
} else {
|
||||
if ("urlContent" in input) {
|
||||
const signal = this.getAbortSignal(options);
|
||||
response = await fetch(
|
||||
`${this.apiUrl}/address?url_content=${input.urlContent}`
|
||||
`${this.apiUrl}/address?url_content=${input.urlContent}`,
|
||||
{ signal },
|
||||
);
|
||||
} else {
|
||||
throw new Error("Input cannot be empty.");
|
||||
|
@ -231,7 +289,7 @@ export class UpEndApi {
|
|||
}
|
||||
|
||||
public async addressToComponents(
|
||||
address: string
|
||||
address: string,
|
||||
): Promise<AddressComponents> {
|
||||
if (!this.wasmExtensions) {
|
||||
throw new Error("WASM extensions not supplied.");
|
||||
|
@ -241,7 +299,7 @@ export class UpEndApi {
|
|||
}
|
||||
|
||||
public async componentsToAddress(
|
||||
components: AddressComponents
|
||||
components: AddressComponents,
|
||||
): Promise<string> {
|
||||
if (!this.wasmExtensions) {
|
||||
throw new Error("WASM extensions not initialized.");
|
||||
|
@ -250,12 +308,18 @@ export class UpEndApi {
|
|||
return this.wasmExtensions.components_to_addr(components);
|
||||
}
|
||||
|
||||
public async getVaultOptions(): Promise<VaultOptions> {
|
||||
const response = await fetch(`${this.apiUrl}/options`);
|
||||
public async getVaultOptions(
|
||||
options?: ApiFetchOptions,
|
||||
): Promise<VaultOptions> {
|
||||
const signal = this.getAbortSignal(options);
|
||||
const response = await fetch(`${this.apiUrl}/options`, { signal });
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
public async setVaultOptions(options: VaultOptions): Promise<void> {
|
||||
public async setVaultOptions(
|
||||
options: VaultOptions,
|
||||
apiOptions?: ApiFetchOptions,
|
||||
): Promise<void> {
|
||||
const payload: Record<string, unknown> = {};
|
||||
|
||||
if (options.blob_mode) {
|
||||
|
@ -264,16 +328,32 @@ export class UpEndApi {
|
|||
payload["blob_mode"] = blob_mode;
|
||||
}
|
||||
|
||||
const signal = this.getAbortSignal(apiOptions);
|
||||
const response = await fetch(`${this.apiUrl}/options`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
signal,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw Error(await response.text());
|
||||
}
|
||||
}
|
||||
|
||||
private getAbortSignal(options: ApiFetchOptions | undefined) {
|
||||
const controller = options?.abortController || new AbortController();
|
||||
const timeout = options?.timeout || this.timeout;
|
||||
if (timeout > 0) {
|
||||
setTimeout(() => controller.abort(), timeout);
|
||||
}
|
||||
return controller.signal;
|
||||
}
|
||||
}
|
||||
|
||||
export interface ApiFetchOptions {
|
||||
timeout?: number;
|
||||
abortController?: AbortController;
|
||||
}
|
||||
|
||||
export type VaultBlobMode = "Flat" | "Mirror" | "Incoming";
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@upnd/upend",
|
||||
"version": "0.2.2",
|
||||
"version": "0.3.2",
|
||||
"description": "Client library to interact with the UpEnd system.",
|
||||
"scripts": {
|
||||
"build": "tsc --build --verbose",
|
||||
|
|
Loading…
Reference in New Issue