[ui] refactor - centralize all `fetch()` calls in `api.ts`

feat/vaults
Tomáš Mládek 2022-02-20 13:06:01 +01:00
parent c2b26ccfee
commit e522e99209
No known key found for this signature in database
GPG Key ID: 65E225C8B3E2ED8A
16 changed files with 249 additions and 193 deletions

View File

@ -25,6 +25,21 @@ export type IValue =
c: null; c: null;
}; };
export interface InvariantEntry {
attribute: string;
value: IValue;
}
export type InAddress =
| Address
| { t: "Attribute" | "Url" | "Uuid"; c?: string };
export type InEntry =
| IEntry
| IEntry[]
| InvariantEntry
| { entity: InAddress };
export interface ListingResult { export interface ListingResult {
[key: string]: IEntry; [key: string]: IEntry;
} }

View File

@ -10,7 +10,7 @@
<script lang="ts"> <script lang="ts">
import { useNavigate } from "svelte-navigator"; import { useNavigate } from "svelte-navigator";
import type { PutResult } from "upend/types"; import { uploadFile } from "../lib/api";
import Icon from "./utils/Icon.svelte"; import Icon from "./utils/Icon.svelte";
import IconButton from "./utils/IconButton.svelte"; import IconButton from "./utils/IconButton.svelte";
const navigate = useNavigate(); const navigate = useNavigate();
@ -30,21 +30,7 @@
try { try {
const responses = await Promise.all( const responses = await Promise.all(
files.map(async (file) => { files.map(async (file) => uploadFile(file))
const formData = new FormData();
formData.append("file", file);
const response = await fetch("api/obj", {
method: "PUT",
body: formData,
});
if (!response.ok) {
throw Error(await response.text());
}
return (await response.json()) as PutResult;
})
); );
const addresses = responses.map(([_, entry]) => entry); const addresses = responses.map(([_, entry]) => entry);

View File

@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from "svelte"; import { createEventDispatcher } from "svelte";
import { putEntityAttribute } from "../lib/api";
import { normUrl } from "../util/history"; import { normUrl } from "../util/history";
import Inspect from "./Inspect.svelte"; import Inspect from "./Inspect.svelte";
@ -19,10 +20,9 @@
window.open(normUrl(`/browse/${address}`), "_blank"); window.open(normUrl(`/browse/${address}`), "_blank");
} }
$: fetch(`api/obj/${address}/LAST_VISITED`, { $: putEntityAttribute(address, "LAST_VISITED", {
method: "PUT", t: "Number",
headers: { "Content-Type": "application/json" }, c: new Date().getTime() / 1000,
body: JSON.stringify({ t: "Number", c: new Date().getTime() / 1000 }),
}); });
</script> </script>

View File

@ -16,6 +16,7 @@
import type { BrowseContext } from "../util/browse"; import type { BrowseContext } from "../util/browse";
import { useParams } from "svelte-navigator"; import { useParams } from "svelte-navigator";
import { GROUP_TYPE_ADDR } from "upend/constants"; import { GROUP_TYPE_ADDR } from "upend/constants";
import { deleteEntry, putEntityAttribute, putEntry } from "../lib/api";
const params = useParams(); const params = useParams();
export let address: string; export let address: string;
@ -119,25 +120,17 @@
const change = ev.detail; const change = ev.detail;
switch (change.type) { switch (change.type) {
case "create": case "create":
await fetch(`api/obj`, { await putEntry({
method: "PUT", entity: address,
headers: { "Content-Type": "application/json" }, attribute: change.attribute,
body: JSON.stringify({ value: change.value,
entity: address,
attribute: change.attribute,
value: change.value,
}),
}); });
break; break;
case "delete": case "delete":
await fetch(`api/obj/${change.address}`, { method: "DELETE" }); await deleteEntry(change.address);
break; break;
case "update": case "update":
await fetch(`api/obj/${address}/${change.attribute}`, { await putEntityAttribute(address, change.attribute, change.value);
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(change.value),
});
break; break;
default: default:
console.error("Unimplemented AttributeChange", change); console.error("Unimplemented AttributeChange", change);
@ -151,34 +144,30 @@
if (!groupToAdd) { if (!groupToAdd) {
return; return;
} }
await fetch(`api/obj`, { await putEntry([
method: "PUT", {
headers: { "Content-Type": "application/json" }, entity: String(groupToAdd.c),
body: JSON.stringify([ attribute: "HAS",
{ value: {
entity: String(groupToAdd.c), t: "Address",
attribute: "HAS", c: address,
value: {
t: "Address",
c: address,
},
}, },
{ },
entity: String(groupToAdd.c), {
attribute: "IS", entity: String(groupToAdd.c),
value: { attribute: "IS",
t: "Address", value: {
c: GROUP_TYPE_ADDR, t: "Address",
}, c: GROUP_TYPE_ADDR,
}, },
]), },
}); ]);
revalidate(); revalidate();
groupToAdd = undefined; groupToAdd = undefined;
} }
async function removeGroup(address: string) { async function removeGroup(address: string) {
await fetch(`api/obj/${address}`, { method: "DELETE" }); await deleteEntry(address);
revalidate(); revalidate();
} }
</script> </script>

View File

@ -5,6 +5,7 @@
import Ellipsis from "../utils/Ellipsis.svelte"; import Ellipsis from "../utils/Ellipsis.svelte";
import UpLink from "./UpLink.svelte"; import UpLink from "./UpLink.svelte";
import { useEntity } from "../../lib/entity"; import { useEntity } from "../../lib/entity";
import { nativeOpen as nativeOpenApi } from "../../lib/api";
import { readable } from "svelte/store"; import { readable } from "svelte/store";
import { notify, UpNotification } from "../../notifications"; import { notify, UpNotification } from "../../notifications";
import IconButton from "../utils/IconButton.svelte"; import IconButton from "../utils/IconButton.svelte";
@ -49,7 +50,7 @@
} in a default native application...` } in a default native application...`
) )
); );
fetch(`api/raw/${address}?native=1`) nativeOpenApi(address)
.then(async (response) => { .then(async (response) => {
if (!response.ok) { if (!response.ok) {
throw new Error(`${response.statusText} - ${await response.text()}`); throw new Error(`${response.statusText} - ${await response.text()}`);

View File

@ -1,6 +1,8 @@
<script lang="ts"> <script lang="ts">
import type { PutResult } from "upend/types"; import type { IEntry } from "upend/types";
import { fetchEntity, useEntity } from "../../../lib/entity";
import { deleteEntry, fetchEntity, putEntry } from "../../../lib/api";
import { useEntity } from "../../../lib/entity";
import Spinner from "../../utils/Spinner.svelte"; import Spinner from "../../utils/Spinner.svelte";
import UpObject from "../UpObject.svelte"; import UpObject from "../UpObject.svelte";
@ -98,87 +100,71 @@
}); });
anno.on("createAnnotation", async (annotation) => { anno.on("createAnnotation", async (annotation) => {
const lensUuidFetch = await fetch("api/obj", { const [_, uuid] = await putEntry({
method: "PUT", entity: {
headers: { "Content-Type": "application/json" }, t: "Uuid",
body: JSON.stringify({ },
entity: {
t: "Uuid",
},
}),
}); });
const [_, uuid] = (await lensUuidFetch.json()) as PutResult;
await fetch("api/obj", { await putEntry([
method: "PUT", {
headers: { "Content-Type": "application/json" }, entity: uuid,
body: JSON.stringify([ attribute: "ANNOTATES",
{ value: {
entity: uuid, t: "Address",
attribute: "ANNOTATES", c: address,
value: {
t: "Address",
c: address,
},
}, },
{ },
{
entity: uuid,
attribute: "W3C_FRAGMENT_SELECTOR",
value: {
t: "String",
c: annotation.target.selector.value,
},
},
...annotation.body.map((body) => {
return {
entity: uuid, entity: uuid,
attribute: "W3C_FRAGMENT_SELECTOR", attribute: "LBL",
value: { value: {
t: "String", t: "String",
c: annotation.target.selector.value, c: body.value,
}, },
}, } as IEntry;
...annotation.body.map((body) => { }),
return { ]);
entity: uuid,
attribute: "LBL",
value: {
t: "String",
c: body.value,
},
};
}),
]),
});
}); });
anno.on("updateAnnotation", async (annotation) => { anno.on("updateAnnotation", async (annotation) => {
const annotationObject = await fetchEntity(annotation.id); const annotationObject = await fetchEntity(annotation.id);
await Promise.all( await Promise.all(
annotationObject.attr["LBL"] annotationObject.attr["LBL"]
.concat(annotationObject.attr["W3C_FRAGMENT_SELECTOR"]) .concat(annotationObject.attr["W3C_FRAGMENT_SELECTOR"])
.map(async (e) => { .map(async (e) => deleteEntry(e.address))
fetch(`api/obj/${e.address}`, { method: "DELETE" });
})
); );
await fetch("api/obj", { await putEntry([
method: "PUT", {
headers: { "Content-Type": "application/json" }, entity: annotation.id,
body: JSON.stringify([ attribute: "W3C_FRAGMENT_SELECTOR",
{ value: {
t: "String",
c: annotation.target.selector.value,
},
},
...annotation.body.map((body) => {
return {
entity: annotation.id, entity: annotation.id,
attribute: "W3C_FRAGMENT_SELECTOR", attribute: "LBL",
value: { value: {
t: "String", t: "String",
c: annotation.target.selector.value, c: body.value,
}, },
}, } as IEntry;
...annotation.body.map((body) => { }),
return { ]);
entity: annotation.id,
attribute: "LBL",
value: {
t: "String",
c: body.value,
},
};
}),
]),
});
}); });
anno.on("deleteAnnotation", async (annotation) => { anno.on("deleteAnnotation", async (annotation) => {
await fetch(`api/obj/${annotation.id}`, { await deleteEntry(annotation.id);
method: "DELETE",
});
}); });
} }
</script> </script>

View File

@ -1,15 +1,13 @@
<script lang="ts"> <script lang="ts">
import { getRaw } from "../../../lib/api";
import IconButton from "../../utils/IconButton.svelte"; import IconButton from "../../utils/IconButton.svelte";
import Spinner from "../../utils/Spinner.svelte"; import Spinner from "../../utils/Spinner.svelte";
export let address: string; export let address: string;
let mode: "preview" | "full" | "markdown" = "preview"; let mode: "preview" | "full" | "markdown" = "preview";
$: textContent = (async () => { $: textContent = (async () => {
const response = await fetch( const response = await getRaw(address, mode == "preview");
`api/${mode == "preview" ? "thumb" : "raw"}/${address}`
);
const text = await response.text(); const text = await response.text();
if (mode === "markdown") { if (mode === "markdown") {
const { marked } = await import("marked"); const { marked } = await import("marked");

View File

@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import { Link, useLocation, useNavigate } from "svelte-navigator"; import { Link, useLocation, useNavigate } from "svelte-navigator";
import { useMatch } from "svelte-navigator"; import { useMatch } from "svelte-navigator";
import { refreshVault } from "../../lib/api";
import { addEmitter } from "../AddModal.svelte"; import { addEmitter } from "../AddModal.svelte";
import Icon from "../utils/Icon.svelte"; import Icon from "../utils/Icon.svelte";
import Input from "../utils/Input.svelte"; import Input from "../utils/Input.svelte";
@ -34,7 +35,7 @@
} }
async function rescan() { async function rescan() {
await fetch("api/refresh", { method: "POST" }); refreshVault();
jobsEmitter.emit("reload"); jobsEmitter.emit("reload");
} }
</script> </script>

View File

@ -11,18 +11,18 @@
import type { IJob } from "upend/types"; import type { IJob } from "upend/types";
import { fade } from "svelte/transition"; import { fade } from "svelte/transition";
import ProgessBar from "../utils/ProgessBar.svelte"; import ProgessBar from "../utils/ProgessBar.svelte";
import { fetchJobs } from "../../lib/api";
interface JobWithId extends IJob { interface JobWithId extends IJob {
id: string; id: string;
} }
let jobs: JobWithId[] = []; let jobs: IJob[] = [];
let activeJobs: JobWithId[] = []; let activeJobs: JobWithId[] = [];
let timeout: number; let timeout: number;
async function updateJobs() { async function updateJobs() {
clearTimeout(timeout); clearTimeout(timeout);
let request = await fetch("api/jobs"); jobs = await fetchJobs();
jobs = await request.json();
activeJobs = Object.entries(jobs) activeJobs = Object.entries(jobs)
.filter(([_, job]) => job.state == "InProgress") .filter(([_, job]) => job.state == "InProgress")

View File

@ -2,6 +2,7 @@
import { debounce } from "lodash"; import { debounce } from "lodash";
import { createEventDispatcher } from "svelte"; import { createEventDispatcher } from "svelte";
import type { IValue, VALUE_TYPE } from "upend/types"; import type { IValue, VALUE_TYPE } from "upend/types";
import { fetchAllAttributes } from "../../lib/api";
import { import {
baseSearchOnce, baseSearchOnce,
createLabelled, createLabelled,
@ -53,8 +54,7 @@
switch (type) { switch (type) {
case "attribute": { case "attribute": {
const req = await fetch("api/all/attributes"); const allAttributes = await fetchAllAttributes();
const allAttributes: string[] = await req.json();
options = allAttributes options = allAttributes
.filter((attr) => attr.toLowerCase().includes(query.toLowerCase())) .filter((attr) => attr.toLowerCase().includes(query.toLowerCase()))
.map((attribute) => { .map((attribute) => {

127
webui/src/lib/api.ts Normal file
View File

@ -0,0 +1,127 @@
import LRU from "lru-cache";
import { UpListing, UpObject } from "upend";
import type {
Address,
IJob,
InEntry,
IValue,
ListingResult,
PutResult,
VaultInfo
} from "upend/types";
import type { EntityListing } from "./entity";
export async function fetchEntity(address: string): Promise<UpObject> {
const entityFetch = await fetch(`api/obj/${address}`);
const entityResult = (await entityFetch.json()) as EntityListing;
const entityListing = new UpListing(entityResult.entries);
return entityListing.getObject(address);
}
export async function fetchEntry(address: string) {
const response = await fetch(`api/raw/${address}`);
const data = await response.json();
const listing = new UpListing({ address: data });
return listing.entries[0];
}
const queryOnceLRU = new LRU<string, UpListing>(128);
const inFlightRequests: { [key: string]: Promise<UpListing> } = {};
export async function queryOnce(query: string): Promise<UpListing> {
const cacheResult = queryOnceLRU.get(query);
if (!cacheResult) {
if (!inFlightRequests[query]) {
console.debug(`Querying: ${query}`);
inFlightRequests[query] = new Promise((resolve, reject) => {
fetch("api/query", { method: "POST", body: query, keepalive: true })
.then(async (response) => {
resolve(new UpListing(await response.json()));
})
.catch((err) => reject(err));
});
} else {
console.debug(`Chaining request for ${query}...`);
}
return await inFlightRequests[query];
} else {
console.debug(`Returning cached: ${query}`);
return cacheResult;
}
}
export async function putEntry(entry: InEntry): Promise<PutResult> {
const response = await fetch(`api/obj`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(entry),
});
return await response.json();
}
export async function putEntityAttribute(
entity: Address,
attribute: string,
value: IValue
): Promise<Address> {
const response = await fetch(`api/obj/${entity}/${attribute}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(value),
});
return await response.json();
}
export async function uploadFile(file: File): Promise<PutResult> {
const formData = new FormData();
formData.append("file", file);
const response = await fetch("api/obj", {
method: "PUT",
body: formData,
});
if (!response.ok) {
throw Error(await response.text());
}
return await response.json();
}
export async function deleteEntry(address: Address): Promise<void> {
await fetch(`api/obj/${address}`, { method: "DELETE" });
}
export async function getRaw(address: Address, preview = false) {
return await fetch(`api/${preview ? "thumb" : "raw"}/${address}`);
}
export async function refreshVault() {
return await fetch("api/refresh", { method: "POST" });
}
export async function nativeOpen(address: Address) {
return fetch(`api/raw/${address}?native=1`);
}
export async function fetchRoots(): Promise<ListingResult> {
const response = await fetch("api/hier_roots");
return await response.json();
}
export async function fetchJobs(): Promise<IJob[]> {
const response = await fetch("api/jobs");
return await response.json();
}
export async function fetchAllAttributes(): Promise<string[]> {
const response = await fetch("api/all/attributes");
return await response.json();
}
export async function fetchInfo(): Promise<VaultInfo> {
const response = await fetch("api/info");
return await response.json();
}

View File

@ -1,5 +1,4 @@
// import { useSWR } from "sswr"; // import { useSWR } from "sswr";
import LRU from "lru-cache";
import { derived, Readable } from "svelte/store"; import { derived, Readable } from "svelte/store";
import { UpListing, UpObject } from "upend"; import { UpListing, UpObject } from "upend";
import type { ListingResult } from "upend/types"; import type { ListingResult } from "upend/types";
@ -19,9 +18,6 @@ export interface EntityListing {
entries: ListingResult; entries: ListingResult;
} }
const queryOnceLRU = new LRU<string, UpListing>(128);
const inFlightRequests: { [key: string]: Promise<UpListing> } = {};
export function useEntity(address: string) { export function useEntity(address: string) {
const { data, error, revalidate } = useSWR<EntityListing, unknown>( const { data, error, revalidate } = useSWR<EntityListing, unknown>(
`api/obj/${address}` `api/obj/${address}`
@ -51,20 +47,6 @@ export function useEntity(address: string) {
}; };
} }
export async function fetchEntity(address: string): Promise<UpObject> {
const entityFetch = await fetch(`api/obj/${address}`);
const entityResult = (await entityFetch.json()) as EntityListing;
const entityListing = new UpListing(entityResult.entries);
return entityListing.getObject(address);
}
export async function fetchEntry(address: string) {
const response = await fetch(`api/raw/${address}`);
const data = await response.json();
const listing = new UpListing({ address: data });
return listing.entries[0];
}
export function query(query: string) { export function query(query: string) {
console.debug(`Querying: ${query}`); console.debug(`Querying: ${query}`);
const { data, error, revalidate } = useSWR<ListingResult, unknown>( const { data, error, revalidate } = useSWR<ListingResult, unknown>(
@ -82,25 +64,3 @@ export function query(query: string) {
revalidate, revalidate,
}; };
} }
export async function queryOnce(query: string): Promise<UpListing> {
const cacheResult = queryOnceLRU.get(query);
if (!cacheResult) {
if (!inFlightRequests[query]) {
console.debug(`Querying: ${query}`);
inFlightRequests[query] = new Promise((resolve, reject) => {
fetch("api/query", { method: "POST", body: query, keepalive: true })
.then(async (response) => {
resolve(new UpListing(await response.json()));
})
.catch((err) => reject(err));
});
} else {
console.debug(`Chaining request for ${query}...`);
}
return await inFlightRequests[query];
} else {
console.debug(`Returning cached: ${query}`);
return cacheResult;
}
}

View File

@ -1,8 +1,9 @@
import { readable, Readable } from "svelte/store"; import { readable, Readable } from "svelte/store";
import type { VaultInfo } from "upend/types"; import type { VaultInfo } from "upend/types";
import { fetchInfo } from "../lib/api";
export const vaultInfo: Readable<VaultInfo> = readable(undefined, (set) => { export const vaultInfo: Readable<VaultInfo> = readable(undefined, (set) => {
fetch("api/info").then(async (response) => { fetchInfo().then(async (info) => {
set(await response.json()); set(info);
}); });
}); });

View File

@ -1,6 +1,7 @@
import type { UpEntry } from "upend"; import type { UpEntry } from "upend";
import type { PutResult } from "upend/types"; import type { InEntry } from "upend/types";
import { query as queryFn, queryOnce } from "../lib/entity"; import { putEntry, queryOnce } from "../lib/api";
import { query as queryFn } from "../lib/entity";
export function baseSearch(query: string) { export function baseSearch(query: string) {
return queryFn( return queryFn(
@ -25,7 +26,7 @@ export async function getObjects(
} }
export async function createLabelled(label: string) { export async function createLabelled(label: string) {
let body: unknown; let body: InEntry;
if (label.match("^[\\w]+://[\\w]")) { if (label.match("^[\\w]+://[\\w]")) {
body = { body = {
entity: { entity: {
@ -43,16 +44,10 @@ export async function createLabelled(label: string) {
}; };
} }
const response = await fetch(`api/obj`, { try {
method: "PUT", const [_, entry] = await putEntry(body);
headers: { "Content-Type": "application/json" }, return entry;
body: JSON.stringify(body), } catch (error) {
}); throw new Error(`Failed to create object: ${error}`);
if (!response.ok) {
throw new Error(`Failed to create object: ${await response.text()}`);
} }
const [_, entry] = (await response.json()) as PutResult;
return entry;
} }

View File

@ -1,19 +1,16 @@
<script lang="ts"> <script lang="ts">
import _ from "lodash";
import { UpListing } from "upend"; import { UpListing } from "upend";
import type { ListingResult } from "upend/types";
import AttributeView from "../components/AttributeView.svelte"; import AttributeView from "../components/AttributeView.svelte";
import UpObjectCard from "../components/display/UpObjectCard.svelte"; import UpObjectCard from "../components/display/UpObjectCard.svelte";
import Spinner from "../components/utils/Spinner.svelte"; import Spinner from "../components/utils/Spinner.svelte";
import { fetchRoots } from "../lib/api";
import { query } from "../lib/entity"; import { query } from "../lib/entity";
import { UpType } from "../lib/types"; import { UpType } from "../lib/types";
import { vaultInfo } from "../util/info"; import { vaultInfo } from "../util/info";
import { updateTitle } from "../util/title"; import { updateTitle } from "../util/title";
const roots = (async () => { const roots = (async () => {
const response = await fetch("api/hier_roots"); const data = await fetchRoots();
const data = (await response.json()) as ListingResult;
const listing = new UpListing(data); const listing = new UpListing(data);
return Object.values(listing.objects) return Object.values(listing.objects)
.filter((obj) => Boolean(obj.attr["LBL"])) .filter((obj) => Boolean(obj.attr["LBL"]))

View File

@ -4669,8 +4669,8 @@ __metadata:
"upend@file:../tools/upend_js::locator=svelte-app%40workspace%3A.": "upend@file:../tools/upend_js::locator=svelte-app%40workspace%3A.":
version: 0.0.1 version: 0.0.1
resolution: "upend@file:../tools/upend_js#../tools/upend_js::hash=fd602e&locator=svelte-app%40workspace%3A." resolution: "upend@file:../tools/upend_js#../tools/upend_js::hash=d7ca1d&locator=svelte-app%40workspace%3A."
checksum: 14cc9cfb6f04a85ec4715b3bbb6b5636b898ccb1ac2418cae9d5e8c00ec25f6245e5be1f8f831c8f24aa3cc0ecb97942c1a569a2fdee104383beb173b40daf0d checksum: 50dc93a08980c68982a46c1c7f59bac5d5e000f2d88a4b8167b4353bafc99c3706375775e78f9fc685a8e03e51cc6c3403714ce4c57ebe5254776dba1ff584b7
languageName: node languageName: node
linkType: hard linkType: hard