From 91b15ad49092c314dd6d3483aec47f0be7a37506 Mon Sep 17 00:00:00 2001 From: polwex Date: Thu, 11 Sep 2025 01:50:29 +0700 Subject: ihategit --- shim/.envrc | 10 +++ shim/.gitignore | 9 ++ shim/devenv.lock | 103 ++++++++++++++++++++++ shim/devenv.nix | 57 +++++++++++++ shim/devenv.yaml | 15 ++++ shim/http-api | 1 + shim/ws-shim/.gitignore | 34 ++++++++ shim/ws-shim/CLAUDE.md | 107 +++++++++++++++++++++++ shim/ws-shim/README.md | 15 ++++ shim/ws-shim/bun.lock | 57 +++++++++++++ shim/ws-shim/index.ts | 1 + shim/ws-shim/package.json | 15 ++++ shim/ws-shim/src/client.ts | 209 +++++++++++++++++++++++++++++++++++++++++++++ shim/ws-shim/src/nostr.ts | 24 ++++++ shim/ws-shim/src/server.ts | 157 ++++++++++++++++++++++++++++++++++ shim/ws-shim/src/test.ts | 44 ++++++++++ shim/ws-shim/src/types.ts | 77 +++++++++++++++++ shim/ws-shim/tsconfig.json | 29 +++++++ wss-shim | 1 - 19 files changed, 964 insertions(+), 1 deletion(-) create mode 100644 shim/.envrc create mode 100644 shim/.gitignore create mode 100644 shim/devenv.lock create mode 100644 shim/devenv.nix create mode 100644 shim/devenv.yaml create mode 120000 shim/http-api create mode 100644 shim/ws-shim/.gitignore create mode 100644 shim/ws-shim/CLAUDE.md create mode 100644 shim/ws-shim/README.md create mode 100644 shim/ws-shim/bun.lock create mode 100644 shim/ws-shim/index.ts create mode 100644 shim/ws-shim/package.json create mode 100644 shim/ws-shim/src/client.ts create mode 100644 shim/ws-shim/src/nostr.ts create mode 100644 shim/ws-shim/src/server.ts create mode 100644 shim/ws-shim/src/test.ts create mode 100644 shim/ws-shim/src/types.ts create mode 100644 shim/ws-shim/tsconfig.json delete mode 160000 wss-shim diff --git a/shim/.envrc b/shim/.envrc new file mode 100644 index 0000000..7e9a2d6 --- /dev/null +++ b/shim/.envrc @@ -0,0 +1,10 @@ +export DIRENV_WARN_TIMEOUT=20s + +eval "$(devenv direnvrc)" + +# `use devenv` supports the same options as the `devenv shell` command. +# +# To silence the output, use `--quiet`. +# +# Example usage: use devenv --quiet --impure --option services.postgres.enable:bool true +use devenv diff --git a/shim/.gitignore b/shim/.gitignore new file mode 100644 index 0000000..4d058db --- /dev/null +++ b/shim/.gitignore @@ -0,0 +1,9 @@ +# Devenv +.devenv* +devenv.local.nix + +# direnv +.direnv + +# pre-commit +.pre-commit-config.yaml diff --git a/shim/devenv.lock b/shim/devenv.lock new file mode 100644 index 0000000..973ead1 --- /dev/null +++ b/shim/devenv.lock @@ -0,0 +1,103 @@ +{ + "nodes": { + "devenv": { + "locked": { + "dir": "src/modules", + "lastModified": 1756101922, + "owner": "cachix", + "repo": "devenv", + "rev": "372c975fd0d5b7fc1ffbb15c75a21d7f9ea97603", + "type": "github" + }, + "original": { + "dir": "src/modules", + "owner": "cachix", + "repo": "devenv", + "type": "github" + } + }, + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1747046372, + "owner": "edolstra", + "repo": "flake-compat", + "rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "git-hooks": { + "inputs": { + "flake-compat": "flake-compat", + "gitignore": "gitignore", + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1755960406, + "owner": "cachix", + "repo": "git-hooks.nix", + "rev": "e891a93b193fcaf2fc8012d890dc7f0befe86ec2", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "git-hooks.nix", + "type": "github" + } + }, + "gitignore": { + "inputs": { + "nixpkgs": [ + "git-hooks", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1709087332, + "owner": "hercules-ci", + "repo": "gitignore.nix", + "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "gitignore.nix", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1755783167, + "owner": "cachix", + "repo": "devenv-nixpkgs", + "rev": "4a880fb247d24fbca57269af672e8f78935b0328", + "type": "github" + }, + "original": { + "owner": "cachix", + "ref": "rolling", + "repo": "devenv-nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "devenv": "devenv", + "git-hooks": "git-hooks", + "nixpkgs": "nixpkgs", + "pre-commit-hooks": [ + "git-hooks" + ] + } + } + }, + "root": "root", + "version": 7 +} diff --git a/shim/devenv.nix b/shim/devenv.nix new file mode 100644 index 0000000..5821877 --- /dev/null +++ b/shim/devenv.nix @@ -0,0 +1,57 @@ +{ + pkgs, + lib, + config, + inputs, + ... +}: { + # https://devenv.sh/basics/ + env.GREET = "devenv"; + + # https://devenv.sh/packages/ + packages = with pkgs; [ + git + nodePackages.prettier + nodePackages.typescript-language-server + ]; + + # https://devenv.sh/languages/ + languages.rust.enable = true; + languages.javascript = { + enable = true; + bun.enable = true; + }; + + # https://devenv.sh/processes/ + # processes.cargo-watch.exec = "cargo-watch"; + + # https://devenv.sh/services/ + # services.postgres.enable = true; + + # https://devenv.sh/scripts/ + scripts.hello.exec = '' + echo hello from $GREET + ''; + + enterShell = '' + hello + git --version + ''; + + # https://devenv.sh/tasks/ + # tasks = { + # "myproj:setup".exec = "mytool build"; + # "devenv:enterShell".after = [ "myproj:setup" ]; + # }; + + # https://devenv.sh/tests/ + enterTest = '' + echo "Running tests" + git --version | grep --color=auto "${pkgs.git.version}" + ''; + + # https://devenv.sh/git-hooks/ + # git-hooks.hooks.shellcheck.enable = true; + + # See full reference at https://devenv.sh/reference/options/ +} diff --git a/shim/devenv.yaml b/shim/devenv.yaml new file mode 100644 index 0000000..116a2ad --- /dev/null +++ b/shim/devenv.yaml @@ -0,0 +1,15 @@ +# yaml-language-server: $schema=https://devenv.sh/devenv.schema.json +inputs: + nixpkgs: + url: github:cachix/devenv-nixpkgs/rolling + +# If you're using non-OSS software, you can set allowUnfree to true. +# allowUnfree: true + +# If you're willing to use a package that's vulnerable +# permittedInsecurePackages: +# - "openssl-1.1.1w" + +# If you have more than one devenv you can merge them +#imports: +# - ./backend diff --git a/shim/http-api b/shim/http-api new file mode 120000 index 0000000..757dc82 --- /dev/null +++ b/shim/http-api @@ -0,0 +1 @@ +/home/y/code/urbit/bun/http-api/ \ No newline at end of file diff --git a/shim/ws-shim/.gitignore b/shim/ws-shim/.gitignore new file mode 100644 index 0000000..a14702c --- /dev/null +++ b/shim/ws-shim/.gitignore @@ -0,0 +1,34 @@ +# dependencies (bun install) +node_modules + +# output +out +dist +*.tgz + +# code coverage +coverage +*.lcov + +# logs +logs +_.log +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# caches +.eslintcache +.cache +*.tsbuildinfo + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/shim/ws-shim/CLAUDE.md b/shim/ws-shim/CLAUDE.md new file mode 100644 index 0000000..630114b --- /dev/null +++ b/shim/ws-shim/CLAUDE.md @@ -0,0 +1,107 @@ +--- + +Default to using Bun instead of Node.js. + +- Use `bun ` instead of `node ` or `ts-node ` +- Use `bun test` instead of `jest` or `vitest` +- Use `bun build ` instead of `webpack` or `esbuild` +- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install` +- Use `bun run + + +``` + +With the following `frontend.tsx`: + +```tsx#frontend.tsx +import React from "react"; + +// import .css files directly and it works +import './index.css'; + +import { createRoot } from "react-dom/client"; + +const root = createRoot(document.body); + +export default function Frontend() { + return

Hello, world!

; +} + +root.render(); +``` + +Then, run index.ts + +```sh +bun --hot ./index.ts +``` + +For more information, read the Bun API docs in `node_modules/bun-types/docs/**.md`. diff --git a/shim/ws-shim/README.md b/shim/ws-shim/README.md new file mode 100644 index 0000000..c1effde --- /dev/null +++ b/shim/ws-shim/README.md @@ -0,0 +1,15 @@ +# ws-shim + +To install dependencies: + +```bash +bun install +``` + +To run: + +```bash +bun run index.ts +``` + +This project was created using `bun init` in bun v1.2.19. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime. diff --git a/shim/ws-shim/bun.lock b/shim/ws-shim/bun.lock new file mode 100644 index 0000000..af9c2cb --- /dev/null +++ b/shim/ws-shim/bun.lock @@ -0,0 +1,57 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "ws-shim", + "dependencies": { + "nostr-tools": "^2.16.2", + "urbit-http": "file:../http-api", + }, + "devDependencies": { + "@types/bun": "latest", + }, + "peerDependencies": { + "typescript": "^5", + }, + }, + }, + "packages": { + "@noble/ciphers": ["@noble/ciphers@0.5.3", "", {}, "sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w=="], + + "@noble/curves": ["@noble/curves@1.2.0", "", { "dependencies": { "@noble/hashes": "1.3.2" } }, "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw=="], + + "@noble/hashes": ["@noble/hashes@1.3.1", "", {}, "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA=="], + + "@scure/base": ["@scure/base@1.1.1", "", {}, "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA=="], + + "@scure/bip32": ["@scure/bip32@1.3.1", "", { "dependencies": { "@noble/curves": "~1.1.0", "@noble/hashes": "~1.3.1", "@scure/base": "~1.1.0" } }, "sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A=="], + + "@scure/bip39": ["@scure/bip39@1.2.1", "", { "dependencies": { "@noble/hashes": "~1.3.0", "@scure/base": "~1.1.0" } }, "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg=="], + + "@types/bun": ["@types/bun@1.2.21", "", { "dependencies": { "bun-types": "1.2.21" } }, "sha512-NiDnvEqmbfQ6dmZ3EeUO577s4P5bf4HCTXtI6trMc6f6RzirY5IrF3aIookuSpyslFzrnvv2lmEWv5HyC1X79A=="], + + "@types/node": ["@types/node@24.3.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow=="], + + "@types/react": ["@types/react@19.1.12", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w=="], + + "bun-types": ["bun-types@1.2.21", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-sa2Tj77Ijc/NTLS0/Odjq/qngmEPZfbfnOERi0KRUYhT9R8M4VBioWVmMWE5GrYbKMc+5lVybXygLdibHaqVqw=="], + + "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + + "nostr-tools": ["nostr-tools@2.16.2", "", { "dependencies": { "@noble/ciphers": "^0.5.1", "@noble/curves": "1.2.0", "@noble/hashes": "1.3.1", "@scure/base": "1.1.1", "@scure/bip32": "1.3.1", "@scure/bip39": "1.2.1", "nostr-wasm": "0.1.0" }, "peerDependencies": { "typescript": ">=5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-ZxH9EbSt5ypURZj2TGNJxZd0Omb5ag5KZSu8IyJMCdLyg2KKz+2GA0sP/cSawCQEkyviIN4eRT4G2gB/t9lMRw=="], + + "nostr-wasm": ["nostr-wasm@0.1.0", "", {}, "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA=="], + + "typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="], + + "undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], + + "urbit-http": ["@urbit/http-api@file:../http-api", { "devDependencies": { "@types/bun": "latest", "typescript": "^5" } }], + + "@noble/curves/@noble/hashes": ["@noble/hashes@1.3.2", "", {}, "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ=="], + + "@scure/bip32/@noble/curves": ["@noble/curves@1.1.0", "", { "dependencies": { "@noble/hashes": "1.3.1" } }, "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA=="], + + "@scure/bip39/@noble/hashes": ["@noble/hashes@1.3.2", "", {}, "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ=="], + } +} diff --git a/shim/ws-shim/index.ts b/shim/ws-shim/index.ts new file mode 100644 index 0000000..f67b2c6 --- /dev/null +++ b/shim/ws-shim/index.ts @@ -0,0 +1 @@ +console.log("Hello via Bun!"); \ No newline at end of file diff --git a/shim/ws-shim/package.json b/shim/ws-shim/package.json new file mode 100644 index 0000000..ff2d0c4 --- /dev/null +++ b/shim/ws-shim/package.json @@ -0,0 +1,15 @@ +{ + "name": "ws-shim", + "module": "index.ts", + "type": "module", + "dependencies": { + "nostr-tools": "^2.16.2", + "urbit-http": "file:../http-api" + }, + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5" + } +} diff --git a/shim/ws-shim/src/client.ts b/shim/ws-shim/src/client.ts new file mode 100644 index 0000000..b7df3e9 --- /dev/null +++ b/shim/ws-shim/src/client.ts @@ -0,0 +1,209 @@ +import type { + NostrEvent, + ClientMessage, + RelayMessage, + Filter, + Subscription, +} from "./types"; + +export class Relay { + private url: string; + private ws: WebSocket | null = null; + private subscriptions: Map = new Map(); + private messageQueue: ClientMessage[] = []; + private reconnectTimer: Timer | null = null; + private reconnectAttempts = 0; + private maxReconnectAttempts = 5; + private reconnectDelay = 1000; + + public status: "connecting" | "connected" | "disconnected" | "error" = + "disconnected"; + + public onconnect?: () => void; + public ondisconnect?: () => void; + public onerror?: (error: Error) => void; + public onnotice?: (message: string) => void; + + constructor(url: string) { + this.url = url; + } + + async connect(): Promise { + return new Promise((resolve, reject) => { + if (this.ws?.readyState === WebSocket.OPEN) { + resolve(); + return; + } + + this.status = "connecting"; + this.ws = new WebSocket(this.url); + + this.ws.onopen = () => { + this.status = "connected"; + this.reconnectAttempts = 0; + this.flushMessageQueue(); + this.onconnect?.(); + resolve(); + }; + + this.ws.onclose = () => { + this.status = "disconnected"; + this.ondisconnect?.(); + this.attemptReconnect(); + }; + + this.ws.onerror = (event) => { + this.status = "error"; + const error = new Error(`WebSocket error: ${event.type}`); + this.onerror?.(error); + reject(error); + }; + + this.ws.onmessage = (event) => { + this.handleMessage(event.data); + }; + }); + } + + disconnect(): void { + if (this.reconnectTimer) { + clearTimeout(this.reconnectTimer); + this.reconnectTimer = null; + } + + if (this.ws) { + this.ws.close(); + this.ws = null; + } + + this.status = "disconnected"; + this.subscriptions.clear(); + this.messageQueue = []; + } + + private attemptReconnect(): void { + if (this.reconnectAttempts >= this.maxReconnectAttempts) { + this.status = "error"; + this.onerror?.(new Error("Max reconnection attempts reached")); + return; + } + + this.reconnectAttempts++; + const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1); + + this.reconnectTimer = setTimeout(() => { + this.connect().catch((error) => { + console.error("Reconnection failed:", error); + }); + }, delay); + } + + private flushMessageQueue(): void { + while (this.messageQueue.length > 0) { + const message = this.messageQueue.shift(); + if (message) { + this.send(message); + } + } + } + + private send(message: ClientMessage): void { + if (this.ws?.readyState === WebSocket.OPEN) { + this.ws.send(JSON.stringify(message)); + } else { + this.messageQueue.push(message); + } + } + + private handleMessage(data: string): void { + try { + const message = JSON.parse(data) as RelayMessage; + + switch (message[0]) { + case "EVENT": { + const [, subscriptionId, event] = message; + const subscription = this.subscriptions.get(subscriptionId); + + if (subscription) { + subscription.onevent?.(event); + } + break; + } + + case "OK": { + const [, eventId, success, messag] = message; + if (!success) { + console.error(`Event ${eventId} rejected: ${messag}`); + } + break; + } + + case "EOSE": { + const [, subscriptionId] = message; + const subscription = this.subscriptions.get(subscriptionId); + subscription?.oneose?.(); + break; + } + + case "CLOSED": { + const [, subscriptionId, messag] = message; + this.subscriptions.delete(subscriptionId); + console.log(`Subscription ${subscriptionId} closed: ${messag}`); + break; + } + + case "NOTICE": { + const [, messag] = message; + this.onnotice?.(messag); + break; + } + + case "AUTH": { + console.warn("AUTH not implemented"); + break; + } + } + } catch (error) { + console.error("Failed to handle message:", error); + } + } + + publishEvent(event: NostrEvent): void { + this.send(["EVENT", event]); + } + + subscribe( + id: string, + filters: Filter[], + handlers: { + onevent?: (event: NostrEvent) => void; + oneose?: () => void; + }, + ): () => void { + const subscription: Subscription = { + id, + filters, + ...handlers, + }; + + this.subscriptions.set(id, subscription); + this.send(["REQ", id, ...filters]); + + return () => { + this.unsubscribe(id); + }; + } + + unsubscribe(id: string): void { + this.subscriptions.delete(id); + this.send(["CLOSE", id]); + } + + getStatus(): string { + return this.status; + } + + getUrl(): string { + return this.url; + } +} diff --git a/shim/ws-shim/src/nostr.ts b/shim/ws-shim/src/nostr.ts new file mode 100644 index 0000000..0b084b6 --- /dev/null +++ b/shim/ws-shim/src/nostr.ts @@ -0,0 +1,24 @@ +import { finalizeEvent, validateEvent, verifyEvent } from "nostr-tools"; +import type { NostrEvent } from "./types"; +import { hexToBytes } from "nostr-tools/utils"; + +export function validate(event: NostrEvent) { + console.log("constructing event in js"); + const priv = + "d862c25aacfae2f66380448eafdeefeccb970a382f2ff185f3e0c5a538d60e35"; + const sk = hexToBytes(priv); + const raw = { + kind: event.kind, + created_at: event.created_at, + tags: event.tags, + content: event.content, + }; + const ev = finalizeEvent(raw, sk); + console.log("js event", ev); + console.log("validating my event", event); + const ok = validateEvent(event); + console.log("is valid?", ok); + const ok2 = verifyEvent(event); + console.log("is verified?", ok2); + return ok && ok2; +} diff --git a/shim/ws-shim/src/server.ts b/shim/ws-shim/src/server.ts new file mode 100644 index 0000000..0b807aa --- /dev/null +++ b/shim/ws-shim/src/server.ts @@ -0,0 +1,157 @@ +import EventEmitter from "events"; +import Urbit from "urbit-http"; + +const SHIP_URL = "http://localhost:8080"; +const api = new Urbit(SHIP_URL, ""); +let sub: number; +// const emitter = new EventEmitter(); + +// //github.com/oven-sh/bun/issues/13811 +// function sse(req: Request, channel: string): Response { +// const stream = new ReadableStream({ +// type: "direct", +// pull(controller: ReadableStreamDirectController) { +// let id = +(req.headers.get("last-event-id") ?? 1); +// const handler = async (event: string, data: unknown): Promise => { +// await controller.write(`id:${id}\n`); +// await controller.write(`event:${event}\n`); +// if (data) await controller.write(`data:${JSON.stringify(data)}\n\n`); +// await controller.flush(); +// id++; +// emitter.on(channel, handler); +// if (req.signal.aborted) { +// emitter.off(channel, handler); +// controller.close(); +// } +// }; +// return new Promise(() => void 0); +// }, +// }); +// return new Response(stream, { +// status: 200, +// headers: { "Content-Type": "text/event-stream" }, +// }); +// } +function emit(channel: string, url: string, event?: any): void { + // emitter.emit(channel, event, data); + const body = JSON.stringify({ event, relay: url }); + fetch(SHIP_URL + "/nostr-shim", { + method: "PUT", + headers: { "Content-type": "application/json" }, + body, + }); +} +const sockets: Map = new Map(); + +const server = Bun.serve({ + //http + routes: { + "/shim": async (req: Request) => { + const data = (await req.json()) as ShimRequest; + console.log("request data", data); + if ("get" in data) { + for (const req of data.get) { + startWSClient(req.relay, req.filters); + } + } + if ("post" in data) { + const ok = validate(data.post.event); + if (!ok) return; + for (const relay of data.post.relays) { + const socket = sockets.get(relay); + if (socket) socket.publishEvent(data.post.event); + else { + await startWSClient(relay, []); + + const socket = sockets.get(relay); + if (socket) socket.publishEvent(data.post.event); + else console.error("wtf man"); + } + } + } + // server.publish("shim", data); + // return sse(req, "all"); + return new Response("OK"); + }, + }, + fetch(req, server) { + const upgraded = server.upgrade(req, { data: { createdAt: Date.now() } }); + if (upgraded) return undefined; + return new Response("henlo"); + }, + websocket: { + // Maximum message size (in bytes) + maxPayloadLength: 64 * 1024, + + // Backpressure limit before messages are dropped + backpressureLimit: 1024 * 1024, + + // Close connection if backpressure limit is hit + closeOnBackpressureLimit: true, + + // Handler called when backpressure is relieved + drain(ws) { + console.log("Backpressure relieved"); + }, + + // Enable per-message deflate compression + perMessageDeflate: { + compress: true, + decompress: true, + }, + + // Send ping frames to keep connection alive + sendPings: true, + + // Handlers for ping/pong frames + ping(ws, data) { + console.log("Received ping"); + }, + pong(ws, data) { + console.log("Received pong"); + }, + + // Whether server receives its own published messages + publishToSelf: false, + // handlers + async open(ws) { + // + // ws.subscribe("shim"); + // console.log(ws, "hey someone subscribed here"); + // if (sub) { + // await api.unsubscribe(sub); + // sub = await api.subscribe({ app: "nostril", path: "/ws" }); + // } else sub = await api.subscribe({ app: "nostril", path: "/ws" }); + }, + async close(ws) { + // + // ws.unsubscribe("shim"); + // if (sub) await api.unsubscribe(sub); + }, + async message(ws) { + // api.poke({ app: "nostril", mark: "json", json: { ws: ws.data } }); + // server.publish("chat", "henlo"); + }, + }, + port: 8888, +}); + +import { Relay } from "./client"; +import type { Filter, ShimRequest } from "./types"; +import { validate } from "./nostr"; +async function startWSClient(url: string, filters: Filter[]) { + console.log("connecting to relay..."); + const relay = new Relay(url); + await relay.connect(); + const id = crypto.randomUUID(); + relay.subscribe(id, filters, { + oneose: () => { + console.log("oneose"); + }, + onevent(event) { + console.log("relay event", { url, event }); + emit("all", url, event); + }, + }); + sockets.set(url, relay); +} diff --git a/shim/ws-shim/src/test.ts b/shim/ws-shim/src/test.ts new file mode 100644 index 0000000..fb87555 --- /dev/null +++ b/shim/ws-shim/src/test.ts @@ -0,0 +1,44 @@ +import { Relay } from "./client"; +const ids = [ + "1a4f2d987384a33753e777138586b1f9b3b62eb0f6e54ca1cdb42859de5625bc", +]; +async function wsClient(url: string) { + console.log("connecting to relae", url); + const relay = new Relay(url); + await relay.connect(); + const id = crypto.randomUUID(); + relay.subscribe(id, [{ ids, limit: 50 }], { + oneose: () => { + console.log("oneose"); + }, + onevent(event) { + console.log("relay event", { url, event }); + }, + }); + // const socket = new WebSocket(url); + // socket.addEventListener("open", (event) => { + // // + // console.log("socket client open", event); + // }); + // socket.addEventListener("close", (event) => { + // // + // console.log("socket client close", event); + // }); + // socket.addEventListener("error", (event) => { + // // + // console.log("socket client error", event); + // }); + // socket.addEventListener("message", (event) => { + // // + // console.log("socket client msg", event); + // }); + // return socket; +} + +const relays = ["wss://nos.lol", "wss://relay.damus.io"]; + +async function run() { + console.log("wth"); + await wsClient(relays[0]!); +} +run(); diff --git a/shim/ws-shim/src/types.ts b/shim/ws-shim/src/types.ts new file mode 100644 index 0000000..4063772 --- /dev/null +++ b/shim/ws-shim/src/types.ts @@ -0,0 +1,77 @@ +// Shim types +export type ShimRequest = { + get: Array<{ relay: string; filters: Filter[] }>; + post: { event: NostrEvent; relays: string[] }; +}; +// NOSTR official +export interface NostrEvent { + id: string; + pubkey: string; + created_at: number; + kind: number; + tags: string[][]; + content: string; + sig: string; +} + +export interface UnsignedEvent { + pubkey: string; + created_at: number; + kind: number; + tags: string[][]; + content: string; +} + +export enum EventKind { + Metadata = 0, + TextNote = 1, + RecommendRelay = 2, + Contacts = 3, + EncryptedDirectMessage = 4, + EventDeletion = 5, + Repost = 6, + Reaction = 7, + BadgeAward = 8, + ChannelCreation = 40, + ChannelMetadata = 41, + ChannelMessage = 42, + ChannelHideMessage = 43, + ChannelMuteUser = 44, +} + +export interface Filter { + ids?: string[]; + authors?: string[]; + kinds?: number[]; + since?: number; + until?: number; + limit?: number; + search?: string; + [key: `#${string}`]: string[]; +} + +export type ClientMessage = + | ["EVENT", NostrEvent] + | ["REQ", string, ...Filter[]] + | ["CLOSE", string]; + +export type RelayMessage = + | ["EVENT", string, NostrEvent] + | ["OK", string, boolean, string] + | ["EOSE", string] + | ["CLOSED", string, string] + | ["NOTICE", string] + | ["AUTH", string]; + +export interface RelayInfo { + url: string; + status: "connecting" | "connected" | "disconnected" | "error"; + ws?: WebSocket; +} + +export interface Subscription { + id: string; + filters: Filter[]; + oneose?: () => void; + onevent?: (event: NostrEvent) => void; +} diff --git a/shim/ws-shim/tsconfig.json b/shim/ws-shim/tsconfig.json new file mode 100644 index 0000000..bfa0fea --- /dev/null +++ b/shim/ws-shim/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + // Environment setup & latest features + "lib": ["ESNext"], + "target": "ESNext", + "module": "Preserve", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +} diff --git a/wss-shim b/wss-shim deleted file mode 160000 index 823d410..0000000 --- a/wss-shim +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 823d410ab3961c035f392563dc02b5cfd9a3d7b9 -- cgit v1.2.3