Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create @lophus/app #40

Merged
merged 10 commits into from
Apr 22, 2024
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,12 @@ for implementation of NIPs, or possibly your own Nostr-like protocols.
General-purpose modules that are developed for Lophus, but not directly related
to the Nostr protocol. You may use them in any TypeScript project.

### [Benchmarks](https://github.com/hasundue/lophus/tree/main/bench)
### [@lophus/app](https://github.com/hasundue/lophus/tree/main/deploy/app)

A SSR-oriented Nostr client application that demonstrates how to use the
library.

### [@lophus/bench](https://github.com/hasundue/lophus/tree/main/bench)

Performance tests for Lophus and other Nostr libraries. Highly experimental.

Expand Down
132 changes: 132 additions & 0 deletions app/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# @lophus/app

_**A Nostr client for hackers and friends**_

This package drives the development of the library while providing a real-world
example of it.

## Architecture

### Features

- No HTML rendering on the front-end (= SSR)
- Backend can be remote (Deno Deploy) or local (Service Worker)
- Remote backend for much less data transfer and CPU usage
- Local backend for more security and censorship resistance
- Fully-customizable multi-column UI with JSON and CSS, which can be shared with
other users

### Entry - Remote backend mode

```mermaid
sequenceDiagram

participant B as Browser
participant S as Server
participant R as Relays (Cache)

critical
B->>+S: GET /
S->>B: index.html
S-->>-B: dist/index.js
note over B: getPublicKey
end

opt Authorization
B->>+S: GET /auth/<pubkey>
S->>-B: <challenge>

B->>+S: POST /auth/<pubkey> Event<22242>
S->>-B: <TOKEN>
end
```

### Entry - Local backend (worker) mode

```mermaid
sequenceDiagram

participant B as Browser
participant W as Worker
participant S as Server
participant R as Relays

critical
B->>+S: GET /?b=local
S->>-B: index.html
end

critical
B->>W: register("/worker.js")
S->>W: dist/worker.js
end

opt
B->>+W: GET, POST, ...
W->>+R: REQ, EVENT, ...
R->>-W: EVENT, ...
W->>-B: Response<html>
end
```

### Configuration

```mermaid
sequenceDiagram

participant B as Browser
participant S as Server / Worker
participant R as Relays / Cache

note over B,S: Entry

opt
B->>+S: POST /users/<pubkey>/config
S->>-R: EVENT<30078>
B->>+S: POST /users/<pubkey>/style
S->>-R: EVENT<30078>
end
```

### Follows feed

```mermaid
sequenceDiagram

participant B as Browser
participant S as Server / Worker
participant R as Relays / Cache

note over B,S: Entry

critical
B->>+S: GET /users/<pubkey>/app
S->>B: app<html>
S-->>-B: dist/app.js
end

opt Get latest events
B->>+S: GET /users/<pubkey>/feeds/<feed>

S->>+R: REQ { kind: 3, author: <pubkey>, limit: 1 }
R->>-S: Event<3>

S-)+R: REQ { kind: 1, limit: N }
loop Receive N events
R-->>-S: Event<1>
end

S->>-B: feed<html>
end

opt Event streaming
B-)+S: GET /users/<pubkey>/feeds/<feed>/events
S->>B: ReadableStream<html>
S-)+R: REQ { kind: 1 }

loop
R-->>-S: Event<1>
S-->>-B: note<html>
end
end
```
17 changes: 17 additions & 0 deletions app/common/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { EventFilter, RelayUrl } from "@lophus/nips/protocol";

export interface Config {
feeds: FeedSpec[];
}

export interface FeedSpec {
name: string;
source: RelayUrl | RelayUrl[] | ScriptSource;
filters?: EventFilter[];
}

export interface RelayMonitorSpec {
name: string;
}

export type ScriptSource = "@lophus/follows";
61 changes: 61 additions & 0 deletions app/common/nostr.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { Relay as _Relay, RelayLike } from "@lophus/nips/relays";
import { RelayGroup, WithPool } from "@lophus/std/relays";
import { EventFilter, EventKind, NostrEvent } from "@lophus/nips";

export class Relay extends WithPool(_Relay) implements RelayLike {}

interface EventSource extends Pick<RelayLike, "config"> {
list<K extends EventKind>(
filter: EventFilter<K>,
): AsyncIterable<NostrEvent<K>>;
get<K extends EventKind>(
filter: EventFilter<K>,
): Promise<NostrEvent<K> | undefined>;
}

interface EventStore extends EventSource {
put<K extends EventKind>(event: NostrEvent<K>): Promise<void>;
}

const knowns = new RelayGroup([
new Relay("wss://nostr.wine"),
]);

/**
* Cache of Nostr events that fallbacks to all the known relays.
*/
export const nostr: EventSource = {
config: { name: "nostr" },
async get(filter) {
return await cache.get(filter) ?? _get(knowns, filter);
},
list(filter) {
return knowns.subscribe(filter);
},
};

export const cache: EventStore = {
config: { name: "cache" },
async get<K extends EventKind>(filter: EventFilter<K>) {
const kv = await Deno.openKv();
if (filter.ids?.length) {
return Promise.any(
filter.ids.map((id) =>
new Promise<NostrEvent<K>>((resolve, reject) =>
kv.get<NostrEvent<K>>(["events", id]).then(({ value }) =>
value ? resolve(value) : reject()
)
)
),
).catch(() => undefined);
}
},
};

async function _get<K extends EventKind>(
source: RelayLike,
filter: EventFilter<K>,
): Promise<NostrEvent<K> | undefined> {
const events = await Array.fromAsync(source.subscribe(filter));
return events.at(0);
}
30 changes: 30 additions & 0 deletions app/common/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { NostrEvent, PublicKey } from "@lophus/core/protocol";
import { RelayLike } from "@lophus/core/relays";

export interface HtmlHandler {
(
props: Record<string, string | undefined>,
specifier?: string,
):
| string
| Promise<string>
| ReadableStream<Uint8Array>
| Promise<ReadableStream<Uint8Array>>;
}

export interface FeederProps {
id?: string;
limit?: number;
me: PublicKey;
source: RelayLike;
}

export interface Feeder {
(
props: FeederProps,
): ReadableStream<NostrEvent> | Promise<ReadableStream<NostrEvent>>;
}

export interface FeederModule {
default: Feeder;
}
13 changes: 13 additions & 0 deletions app/configs/default.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"feeds": [
{
"name": "follows",
"source": "@lophus/follows"
}
],
"relays": {
"wss://nostr.wine": { "read": true, "write": true },
"wss://relay.nostr.wirednet.jp": { "read": true, "write": false }
},
"style": "@lophus/default"
}
15 changes: 15 additions & 0 deletions app/deno.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "@lophus/app",
"version": "0.0.14",
"imports": {
"@std/cli": "jsr:@std/cli@^0.221.0",
"@std/collections": "jsr:@std/collections@^0.221.0",
"@std/path": "jsr:@std/path@^0.221.0",
"@std/streams": "jsr:@std/streams@^0.221.0",
"@lophus/nips": "jsr:@lophus/[email protected]",
"@lophus/std": "jsr:@lophus/[email protected]",
"esbuild": "npm:esbuild@^0.20.2",
"mini-van-plate": "npm:mini-van-plate@^0.5.6",
"sharp": "npm:sharp@^0.33.3"
}
}
Loading
Loading