https://domlink.deployments.hotsocket.fyi/

I did some caching work, I think? Also: increment demo for pro site.

+3 -1
readme
··· 29 29 bro idk what this is anymore but im a fan of it and will keep going 30 30 31 31 i really gotta stop using Node.wraps... 32 - that literally means that my implementation around it is inadequate 32 + that literally means that my implementation around it is inadequate 33 + 34 + i hate git sometimes
+7 -6
src/desktop.ts
··· 1 1 import { Body, Button, Column, Input, Link, Row } from "./domlink.ts"; 2 2 import { List, SocialAppToURI } from "./support/bluesky.ts"; 3 + import { DocProxy } from "./support/caching.ts"; 3 4 import { getUriRecord, resolveMiniDoc } from "./support/slingshot.ts"; 4 5 import { Window } from "./windowing_mod.ts"; 5 6 import { Feed } from "./windows/bluesky/feed.ts"; ··· 14 15 ).closable(false); // haha 15 16 16 17 export const displayAuthorFeed = async (user: string) => { 17 - const doc = await resolveMiniDoc(user); 18 + const doc = await DocProxy.get(user); 18 19 Body.with(new Window("Posts by @"+doc.handle).with( 19 20 await new Feed().loadFeed(Feed.createAuthorGenerator(doc.did)), 20 21 )); ··· 29 30 listInput, 30 31 new Button("Get List Feed", async () => { 31 32 const listURI = await SocialAppToURI(listInput.value, true); 32 - const doc = await resolveMiniDoc(listURI.authority!); 33 + const doc = await DocProxy.get(listURI.authority!); 33 34 const listInfo = await getUriRecord<List>(listURI); 34 35 try{ 35 36 Body.with(new Window(`Posts in "${listInfo.value.name}" by @${doc.handle}`).with( ··· 44 45 const tangledIssuesWindowCreator = new Row().with( 45 46 repoInput, 46 47 new Button("Get Issues", async () => { 47 - const search = new IssueSearch(); 48 + const issueWindow = new Window(); 49 + const search = new IssueSearch(issueWindow); 50 + issueWindow.with(search); 48 51 try { 49 52 await search.getIssues(repoInput.value); 50 53 } catch (e) { 51 54 alert(e); 52 55 } 53 - Body.with(new Window(`Issues in @${search.repoInfo.owner}/${search.repoInfo.name}`).with( 54 - search 55 - )); 56 + Body.with(issueWindow); 56 57 }) 57 58 ); 58 59
+11
src/increment.ts
··· 1 + import { Body, Button, Column, Label } from "@/domlink.ts"; 2 + 3 + const display = new Label("Number: 0"); 4 + let number = 0; 5 + Body.with( 6 + new Column().with( 7 + new Button("Increment!", ()=>{ 8 + display.text = `Number: ${++number}`; 9 + }) 10 + ) 11 + );
+5 -1
src/support/atproto.ts
··· 9 9 return NSIDExpression.test(nsid) ? nsid : null; 10 10 } 11 11 12 + export type Handle = string; 13 + export function StripHandle(handle: Handle) { 14 + return handle.replace("@", ""); 15 + } 12 16 export type DID = `did:${"web"|"plc"}:${string}`; 13 17 export function ValidateDID(did: string): string | null { 14 18 const parts = did.split(":"); ··· 70 74 71 75 // technically you can cast it to whatever you want but i feel like using a generic(?) makes it cleaner 72 76 /** Calls an XRPC "query" method (HTTP GET) 73 - * @param service Defaults to {@link https://slingshot.microcosm.blue Slingshot}. 77 + * @param service Defaults to the {@link https://api.bsky.app/ Bluesky (PBC) API} service. 74 78 */ 75 79 export async function XQuery<T>(method: string, params: Record<string, string | number | null> | null = null, service: string = DEFAULT_SERVICE) { 76 80 let QueryURL = `${service}/xrpc/${method}`;
+8 -7
src/support/bluesky.ts
··· 1 1 import { AppBskyFeedDefs } from "@atcute/bluesky"; 2 2 import { AtURI, DID } from "./atproto.ts"; 3 3 import * as Constellation from "./constellation.ts"; 4 - import { getUriRecord, resolveHandle } from "./slingshot.ts"; 4 + import { getUriRecord } from "./slingshot.ts"; 5 + import { DocProxy } from "./caching.ts"; 5 6 6 7 export type FeedResponse = { 7 8 feed: AppBskyFeedDefs.FeedViewPost[]; ··· 19 20 const url = new URL(link); 20 21 const parts = url.pathname.split("/").splice(1); 21 22 console.log(parts); 22 - let handle = parts[1]; 23 + let id = parts[1]; 23 24 if (resolve) { 24 - handle = (await resolveHandle(handle)).toString(); 25 + id = (await DocProxy.get(id)).handle; 25 26 } 26 27 if (parts[0] == "profile" && parts.length >= 2) { 27 - if (parts.length == 2) return new AtURI(handle); 28 - if (parts.length == 4) return new AtURI(handle, partMap.get(parts[2])!, parts[3]); 28 + if (parts.length == 2) return new AtURI(id); 29 + if (parts.length == 4) return new AtURI(id, partMap.get(parts[2])!, parts[3]); 29 30 } 30 - if (parts[0] == "starter-pack" && parts.length == 3) return new AtURI(handle, "app.bsky.graph.starterpack", parts[2]); 31 + if (parts[0] == "starter-pack" && parts.length == 3) return new AtURI(id, "app.bsky.graph.starterpack", parts[2]); 31 32 throw new Error("URL provided is not under /profile nor /starter-pack"); 32 33 } 33 34 ··· 42 43 createdAt: Date; 43 44 }; 44 45 export async function GetListItems(list: AtURI) { 45 - return (await Promise.all((await Constellation.getLinks(list, "app.bsky.graph.listitem", ".list")) 46 + return (await Promise.all((await Constellation.getLinks(list.toString()!, "app.bsky.graph.listitem", ".list")) 46 47 .map(async x=>await getUriRecord<ListItem>(x)))); 47 48 }
+45
src/support/caching.ts
··· 1 + import { DID, Handle, StripHandle } from "./atproto.ts"; 2 + import { MiniDoc, resolveMiniDoc } from "./slingshot.ts"; 3 + 4 + type Identifier = DID | Handle; 5 + type PromiseCallbacks = { 6 + resolve: (doc: MiniDoc) => void; 7 + reject: (err: unknown) => void; 8 + } 9 + 10 + class _DocCache { 11 + cache: Map<Identifier, MiniDoc> = new Map(); 12 + waiting: Map<Identifier, PromiseCallbacks[]> = new Map(); 13 + async get(k: Identifier): Promise<MiniDoc> { 14 + const key = StripHandle(k); 15 + if (this.cache.has(key)) { 16 + console.debug("cache hit " + key); 17 + return this.cache.get(key)!; 18 + } 19 + 20 + if (this.waiting.has(key)) { 21 + console.debug("cache soft miss " + key); 22 + return new Promise((resolve, reject)=>{ 23 + this.waiting.get(key)!.push({resolve, reject}); 24 + }); 25 + } 26 + 27 + this.waiting.set(key, []); 28 + try { 29 + console.debug("cache hard miss " + key); 30 + const grabbed = await resolveMiniDoc(key); 31 + this.cache.set(key, grabbed); 32 + this.cache.set(grabbed.did, grabbed); 33 + this.cache.set(grabbed.handle, grabbed); 34 + this.waiting.get(key)!.forEach(p => p.resolve(grabbed)); 35 + return grabbed; 36 + } catch (error) { 37 + this.waiting.get(key)!.forEach(p => p.reject(error)); 38 + throw error; 39 + } finally { 40 + this.waiting.delete(key); 41 + } 42 + } 43 + } 44 + 45 + export const DocProxy = new _DocCache();
+39 -10
src/support/tangled.ts
··· 1 - import * as x from "../extras.ts"; 2 - import { AtURI, ListRecords, XQuery } from "./atproto.ts"; 1 + import * as x from "@/extras.ts"; 2 + import { AtURI, DID, GetRecord, ListRecords } from "./atproto.ts"; 3 3 import { getUriRecord, MiniDoc } from "./slingshot.ts"; 4 4 import * as Constellation from "./constellation.ts"; 5 + import { DocProxy } from "./caching.ts"; 5 6 6 7 export type Issue = { 7 8 body: string; 8 9 createdAt: string; 9 10 issueId: number; 10 - owner: string; 11 + owner: DID; 12 + ownerHandle: string; 11 13 repo: string; 12 - title: string; 14 + title: string; 15 + uri: AtURI; // not part of actual issue 16 + }; 17 + export enum IssueStateValues { 18 + Open = "sh.tangled.repo.issue.state.open", 19 + Closed = "sh.tangled.repo.issue.state.closed" 20 + } 21 + export type IssueState = { 22 + issue: AtURI; 23 + state: IssueStateValues; 24 + }; 25 + 26 + export type IssueMeta = { 27 + issue: Issue; 28 + state: IssueState; 13 29 }; 14 30 export type RepoRef = { 15 31 readonly owner: string; ··· 31 47 }; 32 48 33 49 export async function GetRepo(ref: RepoRef): Promise<Repo> { 34 - const repoOwnerDoc = await XQuery<MiniDoc>("com.bad-example.identity.resolveMiniDoc", {identifier: ref.owner}); 50 + const repoOwnerDoc = await DocProxy.get(ref.owner); 35 51 const ownedRepos = await ListRecords<RawRepo>(repoOwnerDoc.did, "sh.tangled.repo", repoOwnerDoc.pds); 36 52 const repoRecord = ownedRepos.records.find(x=>x.value.name == ref.name)!; 37 53 return { ··· 41 57 }; 42 58 } 43 59 export async function GetIssues(repo: Repo, timeout: number = 10000) { 44 - const allIssueRefs = await x.timeout(timeout, Constellation.links(repo.uri, "sh.tangled.repo.issue", ".repo")); 45 - const issues = allIssueRefs.map(async (uri)=>{ 60 + const allIssueRefs = await x.timeout(timeout, Constellation.getLinks(repo.uri.toString()!, "sh.tangled.repo.issue", ".repo")); 61 + const issues = await Promise.all(allIssueRefs.map(async (uri)=>{ 46 62 try { 47 - return (await x.timeout(timeout/5,getUriRecord<Issue>(uri))).value; 63 + const issue = (await x.timeout(timeout/5,getUriRecord<Issue>(uri))).value; 64 + issue.uri = uri; 65 + issue.ownerHandle = (await DocProxy.get(issue.owner)).handle; 66 + return issue; 48 67 } catch { 49 68 console.log(`issue timed out: ${uri.toString()}`); 50 69 return null; 51 70 } 52 - }).filter(x=>x!=null) as unknown as Issue[]; // despite filtering them out, typescript still thinks there could be nulls 53 - return issues.sort((a,b)=>b.issueId-a.issueId); 71 + })) as unknown as Issue[]; // despite filtering them out, typescript still thinks there could be nulls 72 + return issues.filter(x=>x!=null).sort((a,b)=>b.issueId-a.issueId); 73 + } 74 + export async function GetIssueState(issue: Issue) { 75 + const ref = await Constellation.getLinks(issue.uri.toString()!, "sh.tangled.repo.issue.state", ".issue"); 76 + const state = GetRecord<IssueState>(ref[0]); 77 + return state; 78 + } 79 + export async function GetIssueWithMeta(uri: AtURI) { 80 + const issue = (await getUriRecord<Issue>(uri)).value; 81 + const state = await GetIssueState(issue); 82 + return {issue, state}; 54 83 } 55 84 56 85 /** Takes a string like `tangled.sh/core` or `https://tangled.sh/@tangled.sh/core` and returns a {@link RepoRef} */
+3 -1
src/windowing_mod.ts
··· 51 51 } 52 52 53 53 front() { 54 - this.wraps.style.zIndex = `${Window.zIndexCounter++}`; 54 + this.style((s)=>{ 55 + s.zIndex = `${Window.zIndexCounter++}`; 56 + }); 55 57 } 56 58 57 59 override with(...nodes: (Node | string)[]): this {
+1 -1
src/windows/bluesky/feed.ts
··· 1 1 // formerly src/main.ts. how far we've come. 2 2 3 - import { Button, Column, Container, Image, Label, Row } from "@/domlink.ts"; 3 + import { Button, Column, Container, Image, Row } from "@/domlink.ts"; 4 4 import { AppBskyFeedDefs, AppBskyFeedPost } from '@atcute/bluesky'; 5 5 import { AtURI, XQuery } from "@/support/atproto.ts"; 6 6 import { FeedResponse as RawFeedResponse } from "@/support/bluesky.ts";
+18 -25
src/windows/tangled/issuesearch.ts
··· 1 1 // rewrite of @/issuesearch.ts to be a self-contained class that can be shoved into a window easily 2 2 3 3 import { Column, Container, Node, Input, Link, Row, Table, TableCell, TableRow, Button } from "@/domlink.ts"; 4 - import { ListRecords } from "@/support/atproto.ts"; 5 - import { getUriRecord, resolveMiniDoc } from "@/support/slingshot.ts"; 6 - import { Issue, Repo, RepoRecord } from "@/support/tangled.ts"; 7 - import * as Constellation from "@/support/constellation.ts"; 8 - import { timeout } from "@/extras.ts"; 4 + import { GetIssues, GetRepo, Issue, RepoRef } from "@/support/tangled.ts"; 5 + import { Window } from "@/windowing_mod.ts"; 9 6 10 7 export class IssueSearch extends Column { 11 8 repoInput = new Input(); 12 9 searchInput = new Input(); 13 10 searchRow = new Row().with("Search: ", this.searchInput).style(s=>s.display="none"); 14 11 renderOut: Node = new Container().with("<nothing yet>"); 15 - repoInfo: RepoRecord = {} as RepoRecord; 12 + repoInfo: RepoRef = {} as RepoRef; 16 13 issues: Issue[] = []; 17 - constructor() { 14 + window: Window | null = null; 15 + constructor(window: Window | null = null) { 18 16 super(); 17 + if (window) { 18 + this.window = window; 19 + } 19 20 this.class("IssueSearch"); 20 21 this.with( 21 - new Row().with("Repo: ", this.repoInput, new Button("Get", async ()=>{await this.getIssues(this.repoInput.value);})), 22 - this.searchRow, 22 + new Row().with("Repo: ", this.repoInput, new Button("Get", async ()=>{await this.getIssues(this.repoInput.value);}), this.searchRow), 23 23 this.renderOut 24 24 ); 25 25 this.searchInput.watch((search)=>{ ··· 36 36 } 37 37 async getIssues(repo: string) { 38 38 this.repoInfo = IssueSearch.parseRepoInfo(repo); 39 - const doc = await resolveMiniDoc(this.repoInfo.owner); 40 - const ownedRepos = await ListRecords<Repo>(doc.did, "sh.tangled.repo", doc.pds); 41 - const repoRecord = ownedRepos.records.find(x=>x.value.name == this.repoInfo.name)!; 42 - const links = await Constellation.getLinks(repoRecord.uri, "sh.tangled.repo.issue", ".repo"); 43 - this.issues = (await Promise.all(links.map(link => { 44 - try { 45 - return timeout(2000, getUriRecord<Issue>(link)); 46 - } catch { 47 - return null; 48 - } 49 - }))).filter(x=>x).map(x=>x!.value).sort((a,b)=>a.issueId-b.issueId); 39 + this.repoInput.value = `@${this.repoInfo.owner}/${this.repoInfo.name}`; 40 + if (window) this.window!.title = `Issues in @${this.repoInfo.owner}/${this.repoInfo.name}`; 41 + const repoRecord = await GetRepo(this.repoInfo); 42 + this.issues = await GetIssues(repoRecord); 50 43 this.searchRow.style(s=>s.removeProperty("display")); 51 44 this.render(this.issues); 52 45 } ··· 56 49 new TableRow().with( 57 50 new TableCell().with("Handle"), 58 51 new TableCell().with("Issue#"), 59 - new TableCell().with("Title")), 60 - ...issues.map(issue => new TableRow().with( 61 - new TableCell().with(new Link(issue.owner).to(`https://tangled.sh/@${issue.owner}`)), 62 - new TableCell().with(new Link(issue.issueId.toString()).to(`https://tangled.sh/@${this.repoInfo.owner}/${this.repoInfo.name}/issues/${issue.issueId}`)), 52 + new TableCell().with("Title")).style(s=>s.fontWeight="bold"), 53 + ...(issues.map(issue => new TableRow().with( 54 + new TableCell().with(new Link(`@${issue.ownerHandle}`).to(`https://tangled.sh/${issue.ownerHandle}`)), 55 + new TableCell().with(new Link(`${issue.issueId}`).to(`https://tangled.sh/@${this.repoInfo.owner}/${this.repoInfo.name}/issues/${issue.issueId}`)), 63 56 new TableCell().with(issue.title) 64 - )) 57 + ))) 65 58 ); 66 59 this.with(this.renderOut); 67 60 }
+11
static/increment.html
··· 1 + <!DOCTYPE html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="UTF-8"> 5 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 + <title>DomLink Increment Demo</title> 7 + </head> 8 + <body> 9 + <script src="increment.js" type="module"></script> 10 + </body> 11 + </html>