add identify to js lib, change up ui to work with new api

feat/vaults
Tomáš Mládek 2021-12-19 13:54:16 +01:00
parent e7d206beb2
commit e2331981d0
10 changed files with 138 additions and 152 deletions

View File

@ -1,25 +1,19 @@
import type { import type { IEntry, ListingResult, VALUE_TYPE } from "./types";
Address,
IEntry,
ListingResult,
OrderedListing,
VALUE_TYPE,
} from "./types";
export function listingAsOrdered(listing: ListingResult): OrderedListing { // export function listingAsOrdered(listing: ListingResult): OrderedListing {
const entries = Object.entries(listing) as [Address, IEntry][]; // const entries = Object.entries(listing) as [Address, IEntry][];
return entries // return entries
.sort(([_, a], [__, b]) => // .sort(([_, a], [__, b]) =>
String(a.value.c).localeCompare(String(b.value.c)) // String(a.value.c).localeCompare(String(b.value.c))
) // )
.sort(([_, a], [__, b]) => // .sort(([_, a], [__, b]) =>
String(a.value.t).localeCompare(String(b.value.t)) // String(a.value.t).localeCompare(String(b.value.t))
) // )
.sort(([_, a], [__, b]) => a.attribute.localeCompare(b.attribute)); // .sort(([_, a], [__, b]) => a.attribute.localeCompare(b.attribute));
} // }
export class UpListing { export class UpListing {
private entries: UpEntry[]; public readonly entries: UpEntry[];
constructor(listing: ListingResult) { constructor(listing: ListingResult) {
this.entries = Object.entries(listing).map((lr) => new UpEntry(...lr)); this.entries = Object.entries(listing).map((lr) => new UpEntry(...lr));
@ -46,17 +40,27 @@ export class UpObject {
this.entries = entries; this.entries = entries;
} }
public bindAppend(entries: UpEntry[]) {
this.entries.push(...entries);
}
public get attributes() {
return this.entries.filter((e) => e.entity === this.address);
}
public get backlinks() {
return this.entries.filter((e) => e.value.c === this.address);
}
public get attr() { public get attr() {
const result = {} as { [key: string]: UpEntry[] }; const result = {} as { [key: string]: UpEntry[] };
this.entries this.attributes.forEach((entry) => {
.filter((e) => e.entity == this.address) if (!result[entry.attribute]) {
.forEach((entry) => { result[entry.attribute] = [];
if (!result[entry.attribute]) { }
result[entry.attribute] = [];
}
result[entry.attribute].push(entry); result[entry.attribute].push(entry);
}); });
return result; return result;
} }
@ -65,6 +69,25 @@ export class UpObject {
return this.attr[attr] ? this.attr[attr][0].value.c : undefined; return this.attr[attr] ? this.attr[attr][0].value.c : undefined;
} }
public identify(): string[] {
// Get all places where this Object is "had"
const hasEntries = this.backlinks
.filter((entry) => entry.attribute === "HAS")
.map((entry) => entry.address);
// Out of those relations, retrieve their ALIAS attrs
const hasAliases = this.entries
.filter(
(entry) =>
entry.attribute === "ALIAS" && hasEntries.includes(entry.entity)
)
.map((entry) => String(entry.value.c));
const lblValues = (this.attr["LBL"] || []).map((e) => String(e.value.c));
return lblValues.concat(hasAliases);
}
public asDict() { public asDict() {
return { return {
address: this.address, address: this.address,

View File

@ -11,7 +11,7 @@ export interface ListingResult {
[key: string]: IEntry; [key: string]: IEntry;
} }
export type OrderedListing = [Address, IEntry][]; // export type OrderedListing = [Address, IEntry][];
export interface Job { export interface Job {
title: string; title: string;

View File

@ -1,10 +1,10 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from "svelte"; import { createEventDispatcher } from "svelte";
import { BLOB_TYPE_ADDR } from "upend/constants"; import { BLOB_TYPE_ADDR } from "upend/constants";
import { identify, useEntity } from "../lib/entity";
import HashBadge from "./HashBadge.svelte"; import HashBadge from "./HashBadge.svelte";
import Ellipsis from "./Ellipsis.svelte"; import Ellipsis from "./Ellipsis.svelte";
import UpLink from "./UpLink.svelte"; import UpLink from "./UpLink.svelte";
import { useEntity } from "../lib/entity";
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
export let address: string; export let address: string;
@ -14,22 +14,18 @@
let resolving = resolve; let resolving = resolve;
$: ({ attributes, backlinks } = useEntity(address, () => resolve)); $: ({ entity } = useEntity(address));
// isFile // isFile
$: isFile = $attributes.some( $: isFile = $entity?.get("IS") === BLOB_TYPE_ADDR;
([_, attr]) => attr.attribute === "IS" && attr.value.c === BLOB_TYPE_ADDR
);
// Identification // Identification
let inferredIds: string[] = []; let inferredIds: string[] = [];
$: { $: {
if (resolve) { if (resolve) {
resolving = true; resolving = true;
identify($attributes, $backlinks).then((inferredEntries) => { inferredIds = $entity?.identify() || [];
inferredIds = inferredEntries; resolving = false;
resolving = false;
});
} }
} }
@ -41,19 +37,19 @@
<div class="address" class:identified={Boolean(inferredIds)} class:banner> <div class="address" class:identified={Boolean(inferredIds)} class:banner>
<HashBadge {address} /> <HashBadge {address} />
<div class="separator" /> <div class="separator" />
<div class="label" class:resolving title={label}> <div class="label" class:resolving title={label}>
{#if banner && isFile} {#if banner && isFile}
<a href="/api/raw/{address}" target="_blank"> <a href="/api/raw/{address}" target="_blank">
<Ellipsis value={label} /> <Ellipsis value={label} />
</a> </a>
{:else if link} {:else if link}
<UpLink to={{ entity: address }}> <UpLink to={{ entity: address }}>
<Ellipsis value={label} /> <Ellipsis value={label} />
</UpLink> </UpLink>
{:else} {:else}
<Ellipsis value={label} /> <Ellipsis value={label} />
{/if} {/if}
</div> </div>
</div> </div>
<style scoped lang="scss"> <style scoped lang="scss">

View File

@ -1,14 +1,14 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from "svelte"; import { createEventDispatcher } from "svelte";
import type { IEntry } from "upend/types";
import UpLink from "./UpLink.svelte"; import UpLink from "./UpLink.svelte";
import type { Component, UpType, Widget } from "../lib/types"; import type { Component, UpType, Widget } from "../lib/types";
import Table from "./widgets/Table.svelte"; import Table from "./widgets/Table.svelte";
import TableComponent from "./widgets/Table.svelte"; // silence false svelte(reactive-component) warnings import TableComponent from "./widgets/Table.svelte"; // silence false svelte(reactive-component) warnings
import type { AttributeChange } from "../types/base"; import type { AttributeChange } from "../types/base";
import type { UpEntry } from "upend";
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
export let attributes: [string, IEntry][]; export let attributes: UpEntry[];
export let type: UpType | undefined = undefined; export let type: UpType | undefined = undefined;
export let address: String; export let address: String;
export let title: String | undefined = undefined; export let title: String | undefined = undefined;

View File

@ -2,10 +2,9 @@
import { useEntity } from "../lib/entity"; import { useEntity } from "../lib/entity";
export let address: string; export let address: string;
$: ({ attributes } = useEntity(address)); $: ({ entity } = useEntity(address));
$: mimeType = String($attributes.find(([_, e]) => e.attribute === "FILE_MIME")?.[1] $: mimeType = String($entity?.get("FILE_MIME"));
.value.c);
</script> </script>
<div class="preview" v-if="mimeType"> <div class="preview" v-if="mimeType">

View File

@ -7,6 +7,7 @@
import BlobPreview from "./BlobPreview.svelte"; import BlobPreview from "./BlobPreview.svelte";
import { setContext } from "svelte"; import { setContext } from "svelte";
import { writable } from "svelte/store"; import { writable } from "svelte/store";
import type { UpEntry } from "upend";
export let address: string; export let address: string;
export let index: number | undefined; export let index: number | undefined;
@ -17,12 +18,9 @@
setContext("browse", { index: indexStore }); setContext("browse", { index: indexStore });
$: ({ error, revalidate, attributes, backlinks } = useEntity(address)); $: ({ entity, error, revalidate } = useEntity(address));
$: allTypeAddresses = $attributes $: allTypeAddresses = ($entity?.attr["IS"] || []).map((attr) => attr.value.c);
.map(([_, attr]) => attr)
.filter((attr) => attr.attribute == "IS")
.map((attr) => attr.value.c);
$: allTypeEntries = query( $: allTypeEntries = query(
() => () =>
@ -34,19 +32,19 @@
let allTypes: { [key: string]: UpType } = {}; let allTypes: { [key: string]: UpType } = {};
$: { $: {
allTypes = {}; allTypes = {};
$allTypeEntries.forEach(([_, entry]) => { ($allTypeEntries?.entries || []).forEach((entry) => {
if (allTypes[entry.entity] === undefined) { if (allTypes[entry.entity] === undefined) {
allTypes[entry.entity] = new UpType(entry.entity); allTypes[entry.entity] = new UpType(entry.entity);
} }
switch (entry.attribute) { switch (entry.attribute) {
case "TYPE": case "TYPE":
allTypes[entry.entity].name = entry.value.c; allTypes[entry.entity].name = String(entry.value.c);
break; break;
case "TYPE_HAS": case "TYPE_HAS":
case "TYPE_REQUIRES": case "TYPE_REQUIRES":
case "TYPE_ID": case "TYPE_ID":
allTypes[entry.entity].attributes.push(entry.value.c); allTypes[entry.entity].attributes.push(String(entry.value.c));
break; break;
} }
}); });
@ -54,14 +52,14 @@
allTypes = allTypes; allTypes = allTypes;
} }
let typedAttributes = {} as { [key: string]: [string, IEntry][] }; let typedAttributes = {} as { [key: string]: UpEntry[] };
let untypedAttributes = [] as [string, IEntry][]; let untypedAttributes = [] as UpEntry[];
$: { $: {
typedAttributes = {}; typedAttributes = {};
untypedAttributes = []; untypedAttributes = [];
$attributes.forEach(([entryAddr, entry]) => { ($entity?.attributes || []).forEach((entry) => {
const entryTypes = Object.entries(allTypes).filter(([_, t]) => const entryTypes = Object.entries(allTypes).filter(([_, t]) =>
t.attributes.includes(entry.attribute) t.attributes.includes(entry.attribute)
); );
@ -70,10 +68,10 @@
if (typedAttributes[addr] == undefined) { if (typedAttributes[addr] == undefined) {
typedAttributes[addr] = []; typedAttributes[addr] = [];
} }
typedAttributes[addr].push([entryAddr, entry]); typedAttributes[addr].push(entry);
}); });
} else { } else {
untypedAttributes.push([entryAddr, entry]); untypedAttributes.push(entry);
} }
}); });
@ -82,9 +80,9 @@
} }
$: filteredUntypedAttributes = untypedAttributes.filter( $: filteredUntypedAttributes = untypedAttributes.filter(
([_, entry]) => (entry) =>
entry.attribute !== "IS" || entry.attribute !== "IS" ||
!Object.keys(typedAttributes).includes(entry.value.c) !Object.keys(typedAttributes).includes(String(entry.value.c))
); );
</script> </script>
@ -113,11 +111,11 @@
on:changed={revalidate} on:changed={revalidate}
/> />
{/if} {/if}
{#if $backlinks.length > 0} {#if $entity?.backlinks.length > 0}
<AttributeView <AttributeView
title={`Referred to (${$backlinks.length})`} title={`Referred to (${$entity.backlinks.length})`}
{address} {address}
attributes={$backlinks} attributes={$entity.backlinks}
reverse reverse
on:changed={revalidate} on:changed={revalidate}
/> />

View File

@ -1,13 +1,14 @@
<script lang="ts"> <script lang="ts">
import type { OrderedListing } from "upend/types"; import type { UpEntry } from "upend";
import Table from "./Table.svelte"; import Table from "./Table.svelte";
export let attributes: OrderedListing; export let attributes: UpEntry[];
export let attribute: string; export let attribute: string;
export let editable = false; export let editable = false;
$: filteredAttributes = attributes.filter( $: filteredAttributes = attributes.filter(
([_, entry]) => entry.attribute === attribute (entry) => entry.attribute === attribute
); );
</script> </script>

View File

@ -1,20 +1,20 @@
<script lang="ts"> <script lang="ts">
import filesize from "filesize"; import filesize from "filesize";
import { format, fromUnixTime } from "date-fns"; import { format, fromUnixTime } from "date-fns";
import type { OrderedListing } from "upend/types";
import Ellipsis from "../Ellipsis.svelte"; import Ellipsis from "../Ellipsis.svelte";
import Address from "../Address.svelte"; import Address from "../Address.svelte";
import { createEventDispatcher, getContext } from "svelte"; import { createEventDispatcher, getContext } from "svelte";
import type { AttributeChange } from "../../types/base"; import type { AttributeChange } from "../../types/base";
import { useParams } from "svelte-navigator"; import { useParams } from "svelte-navigator";
import type { Writable } from "svelte/store"; import type { Writable } from "svelte/store";
import type { UpEntry } from "upend";
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
const params = useParams(); const params = useParams();
export let columns = "attribute, value"; export let columns = "attribute, value";
export let header = true; export let header = true;
export let attributes: OrderedListing; export let attributes: UpEntry[];
export let editable = false; export let editable = false;
// Display // Display
@ -61,7 +61,7 @@
let sortKeys: { [key: string]: string } = {}; let sortKeys: { [key: string]: string } = {};
$: sortedAttributes = attributes $: sortedAttributes = attributes
.concat() .concat()
.sort(([_, aEntry], [__, bEntry]) => { .sort((aEntry, bEntry) => {
if ( if (
sortKeys[aEntry.value.c] === undefined || sortKeys[aEntry.value.c] === undefined ||
sortKeys[bEntry.value.c] === undefined sortKeys[bEntry.value.c] === undefined
@ -71,16 +71,16 @@
} else if (!sortKeys[aEntry.value.c] && sortKeys[bEntry.value.c]) { } else if (!sortKeys[aEntry.value.c] && sortKeys[bEntry.value.c]) {
return 1; return 1;
} else { } else {
return aEntry.value.c.localeCompare(bEntry.value.c); return String(aEntry.value.c).localeCompare(String(bEntry.value.c));
} }
} else { } else {
return sortKeys[aEntry.value.c].localeCompare(sortKeys[bEntry.value.c]); return sortKeys[aEntry.value.c].localeCompare(sortKeys[bEntry.value.c]);
} }
}) })
.sort(([_, aEntry], [__, bEntry]) => { .sort((aEntry, bEntry) => {
return aEntry.attribute.localeCompare(bEntry.attribute); return aEntry.attribute.localeCompare(bEntry.attribute);
}) })
.sort(([_, aEntry], [__, bEntry]) => { .sort((aEntry, bEntry) => {
if ( if (
sortKeys[aEntry.entity] === undefined || sortKeys[aEntry.entity] === undefined ||
sortKeys[bEntry.entity] === undefined sortKeys[bEntry.entity] === undefined
@ -108,16 +108,21 @@
FILE_SIZE: "File size", FILE_SIZE: "File size",
}; };
const VALUE_FORMATTERS: { [key: string]: (val: string) => string } = { const VALUE_FORMATTERS: { [key: string]: (val: string | number) => string } =
FILE_MTIME: (val) => format(fromUnixTime(parseInt(val, 10)), "PPpp"), {
FILE_SIZE: (val) => filesize(parseInt(val, 10), { base: 2 }), FILE_MTIME: (val) =>
}; format(fromUnixTime(parseInt(String(val), 10)), "PPpp"),
FILE_SIZE: (val) => filesize(parseInt(String(val), 10), { base: 2 }),
};
function formatAttribute(attribute: string) { function formatAttribute(attribute: string) {
return ATTRIBUTE_LABELS[attribute]; return ATTRIBUTE_LABELS[attribute];
} }
function formatValue(value: string, attribute: string): string | undefined { function formatValue(
value: string | number,
attribute: string
): string | undefined {
const handler = VALUE_FORMATTERS[attribute]; const handler = VALUE_FORMATTERS[attribute];
if (handler) { if (handler) {
return handler(value); return handler(value);
@ -161,7 +166,7 @@
</tr> </tr>
{/if} {/if}
{#each sortedAttributes as [id, entry] (id)} {#each sortedAttributes as entry (entry.address)}
<tr <tr
class:left-active={entry.entity == addresses[$index - 1] || class:left-active={entry.entity == addresses[$index - 1] ||
entry.value.c == addresses[$index - 1]} entry.value.c == addresses[$index - 1]}
@ -170,7 +175,10 @@
> >
{#if editable} {#if editable}
<td class="attr-action"> <td class="attr-action">
<sl-icon-button name="x-circle" on:click={removeEntry(id)} /> <sl-icon-button
name="x-circle"
on:click={removeEntry(entry.address)}
/>
</td> </td>
{/if} {/if}
@ -199,13 +207,13 @@
<text-input <text-input
{editable} {editable}
value={entry.value.c} value={entry.value.c}
on:edit={(val) => updateEntry(id, entry.attribute, val)} on:edit={(val) => updateEntry(entry.address, entry.attribute, val)}
> >
{#if entry.value.t === "Address"} {#if entry.value.t === "Address"}
<Address <Address
link link
address={entry.value.c} address={String(entry.value.c)}
resolve={Boolean(resolve[id]) || true} resolve={Boolean(resolve[entry.address]) || true}
on:resolved={(event) => { on:resolved={(event) => {
sortKeys[entry.value.c] = event.detail[0]; sortKeys[entry.value.c] = event.detail[0];
}} }}
@ -218,7 +226,7 @@
> >
<Ellipsis <Ellipsis
value={formatValue(entry.value.c, entry.attribute) || value={formatValue(entry.value.c, entry.attribute) ||
entry.value.c} String(entry.value.c)}
/> />
</div> </div>
{/if} {/if}

View File

@ -1,37 +1,27 @@
// import { useSWR } from "sswr"; // import { useSWR } from "sswr";
import { useSWR } from "../util/fetch";
import { derived, Readable, readable, writable } from "svelte/store";
import type { IEntry, ListingResult, OrderedListing } from "upend/types";
import { listingAsOrdered, UpEntry } from "upend";
import LRU from "lru-cache"; import LRU from "lru-cache";
import { derived, Readable, Writable, writable } from "svelte/store";
import { UpListing, UpObject } from "upend";
import type { ListingResult } from "upend/types";
import { useSWR } from "../util/fetch";
export function useEntity( const queryOnceLRU = new LRU<string, UpListing>(128);
address: string | (() => string), const inFlightRequests: { [key: string]: Promise<UpListing> } = {};
condition?: () => Boolean
) { export function useEntity(address: string) {
const { data, error, revalidate } = useSWR<ListingResult, unknown>(() => const { data, error, revalidate } = useSWR<ListingResult, unknown>(
condition === undefined || condition() () => `/api/obj/${address}`
? `/api/obj/${typeof address === "string" ? address : address()}`
: null
); );
const entries = derived(data, ($values) => const entity: Readable<UpObject | undefined> = derived(data, ($listing) => {
$values ? listingAsOrdered($values) : [] if ($listing) {
); const listing = new UpListing($listing);
const attributes = derived(entries, ($entries) => { return listing.objects.find((obj) => obj.address == address);
const addr = typeof address === "string" ? address : address(); }
return $entries.filter(([_, e]) => e.entity === addr);
});
const backlinks = derived(entries, ($entries) => {
const addr = typeof address === "string" ? address : address();
return $entries.filter(([_, e]) => e.entity !== addr);
}); });
return { return {
entries, entity,
attributes,
backlinks,
data,
error, error,
revalidate, revalidate,
}; };
@ -45,7 +35,7 @@ export function query(query: () => string) {
); );
const result = derived(data, ($values) => { const result = derived(data, ($values) => {
return $values ? listingAsOrdered($values) : []; return $values ? new UpListing($values) : undefined;
}); });
return { return {
@ -56,19 +46,15 @@ export function query(query: () => string) {
}; };
} }
const queryOnceLRU = new LRU<string, OrderedListing>(128); export async function queryOnce(query: string): Promise<UpListing> {
const inFlightRequests: { [key: string]: Promise<OrderedListing> } = {};
export async function queryOnce(query: string): Promise<OrderedListing> {
const cacheResult = queryOnceLRU.get(query); const cacheResult = queryOnceLRU.get(query);
if (!cacheResult) { if (!cacheResult) {
const url = `/api/obj?query=${query}`; const url = `/api/obj?query=${query}`;
let response;
if (!inFlightRequests[url]) { if (!inFlightRequests[url]) {
console.debug(`Querying: ${query}`); console.debug(`Querying: ${query}`);
inFlightRequests[url] = new Promise(async (resolve, reject) => { inFlightRequests[url] = new Promise(async (resolve, reject) => {
const response = await fetch(url, { keepalive: true }); const response = await fetch(url, { keepalive: true });
resolve(listingAsOrdered(await response.json())); resolve(new UpListing(await response.json()));
}); });
} else { } else {
console.debug(`Chaining request for ${query}...`); console.debug(`Chaining request for ${query}...`);
@ -79,28 +65,3 @@ export async function queryOnce(query: string): Promise<OrderedListing> {
return cacheResult; return cacheResult;
} }
} }
export async function identify(
attributes: UpEntry[],
backlinks: UpEntry[]
): Promise<string[]> {
// Get all entries where the object is linked
const hasEntries = backlinks
.filter((entry) => entry.attribute === "HAS")
.map((entry) => entry.address);
// Out of those relations, retrieve their ALIAS attrs
const hasAliases = hasEntries.length
? await queryOnce(
`(matches (in ${hasEntries.map((e) => `"${e}"`).join(" ")}) "ALIAS" ?)`
)
: [];
const aliasValues = hasAliases.map(([_, entry]) => String(entry.value.c));
// Return all LBLs concatenated with named aliases
return attributes
.filter((attr) => attr.attribute === "LBL")
.map((attr) => String(attr.value.c))
.concat(aliasValues);
}

View File

@ -3301,8 +3301,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=0110ea&locator=svelte-app%40workspace%3A." resolution: "upend@file:../tools/upend_js#../tools/upend_js::hash=88b9af&locator=svelte-app%40workspace%3A."
checksum: 52a706bb4738034b16ec95a0e569fc38597177fadc0a2b327ac59889b19b06d032554e0e4f47b978416f65ac96341ad5c4b6bf2c56a59d0bdafc4bcfabf1681b checksum: 9076efc8b84c0c96f5d48ce092320832be93d18529af48d6ab623a3adb698401b78913ceac95439ddaaf3232a6a41ec886ad263ce2f05646ad6460d98627f8e9
languageName: node languageName: node
linkType: hard linkType: hard