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 {
Address,
IEntry,
ListingResult,
OrderedListing,
VALUE_TYPE,
} from "./types";
import type { IEntry, ListingResult, VALUE_TYPE } from "./types";
export function listingAsOrdered(listing: ListingResult): OrderedListing {
const entries = Object.entries(listing) as [Address, IEntry][];
return entries
.sort(([_, a], [__, b]) =>
String(a.value.c).localeCompare(String(b.value.c))
)
.sort(([_, a], [__, b]) =>
String(a.value.t).localeCompare(String(b.value.t))
)
.sort(([_, a], [__, b]) => a.attribute.localeCompare(b.attribute));
}
// export function listingAsOrdered(listing: ListingResult): OrderedListing {
// const entries = Object.entries(listing) as [Address, IEntry][];
// return entries
// .sort(([_, a], [__, b]) =>
// String(a.value.c).localeCompare(String(b.value.c))
// )
// .sort(([_, a], [__, b]) =>
// String(a.value.t).localeCompare(String(b.value.t))
// )
// .sort(([_, a], [__, b]) => a.attribute.localeCompare(b.attribute));
// }
export class UpListing {
private entries: UpEntry[];
public readonly entries: UpEntry[];
constructor(listing: ListingResult) {
this.entries = Object.entries(listing).map((lr) => new UpEntry(...lr));
@ -46,17 +40,27 @@ export class UpObject {
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() {
const result = {} as { [key: string]: UpEntry[] };
this.entries
.filter((e) => e.entity == this.address)
.forEach((entry) => {
if (!result[entry.attribute]) {
result[entry.attribute] = [];
}
this.attributes.forEach((entry) => {
if (!result[entry.attribute]) {
result[entry.attribute] = [];
}
result[entry.attribute].push(entry);
});
result[entry.attribute].push(entry);
});
return result;
}
@ -65,6 +69,25 @@ export class UpObject {
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() {
return {
address: this.address,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,37 +1,27 @@
// 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 { 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(
address: string | (() => string),
condition?: () => Boolean
) {
const { data, error, revalidate } = useSWR<ListingResult, unknown>(() =>
condition === undefined || condition()
? `/api/obj/${typeof address === "string" ? address : address()}`
: null
const queryOnceLRU = new LRU<string, UpListing>(128);
const inFlightRequests: { [key: string]: Promise<UpListing> } = {};
export function useEntity(address: string) {
const { data, error, revalidate } = useSWR<ListingResult, unknown>(
() => `/api/obj/${address}`
);
const entries = derived(data, ($values) =>
$values ? listingAsOrdered($values) : []
);
const attributes = derived(entries, ($entries) => {
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);
const entity: Readable<UpObject | undefined> = derived(data, ($listing) => {
if ($listing) {
const listing = new UpListing($listing);
return listing.objects.find((obj) => obj.address == address);
}
});
return {
entries,
attributes,
backlinks,
data,
entity,
error,
revalidate,
};
@ -45,7 +35,7 @@ export function query(query: () => string) {
);
const result = derived(data, ($values) => {
return $values ? listingAsOrdered($values) : [];
return $values ? new UpListing($values) : undefined;
});
return {
@ -56,19 +46,15 @@ export function query(query: () => string) {
};
}
const queryOnceLRU = new LRU<string, OrderedListing>(128);
const inFlightRequests: { [key: string]: Promise<OrderedListing> } = {};
export async function queryOnce(query: string): Promise<OrderedListing> {
export async function queryOnce(query: string): Promise<UpListing> {
const cacheResult = queryOnceLRU.get(query);
if (!cacheResult) {
const url = `/api/obj?query=${query}`;
let response;
if (!inFlightRequests[url]) {
console.debug(`Querying: ${query}`);
inFlightRequests[url] = new Promise(async (resolve, reject) => {
const response = await fetch(url, { keepalive: true });
resolve(listingAsOrdered(await response.json()));
resolve(new UpListing(await response.json()));
});
} else {
console.debug(`Chaining request for ${query}...`);
@ -79,28 +65,3 @@ export async function queryOnce(query: string): Promise<OrderedListing> {
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.":
version: 0.0.1
resolution: "upend@file:../tools/upend_js#../tools/upend_js::hash=0110ea&locator=svelte-app%40workspace%3A."
checksum: 52a706bb4738034b16ec95a0e569fc38597177fadc0a2b327ac59889b19b06d032554e0e4f47b978416f65ac96341ad5c4b6bf2c56a59d0bdafc4bcfabf1681b
resolution: "upend@file:../tools/upend_js#../tools/upend_js::hash=88b9af&locator=svelte-app%40workspace%3A."
checksum: 9076efc8b84c0c96f5d48ce092320832be93d18529af48d6ab623a3adb698401b78913ceac95439ddaaf3232a6a41ec886ad263ce2f05646ad6460d98627f8e9
languageName: node
linkType: hard