+3
-1
readme
+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
+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
+11
src/increment.ts
+5
-1
src/support/atproto.ts
+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
+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
+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
+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
+3
-1
src/windowing_mod.ts
+1
-1
src/windows/bluesky/feed.ts
+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
+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
+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>