This commit is contained in:
Jon Staab
2025-04-09 11:35:09 -07:00
parent 5f3624b8f3
commit 728ad1fba0
37 changed files with 1039 additions and 2183 deletions
+1 -2
View File
@@ -3,6 +3,5 @@
"printWidth": 100, "printWidth": 100,
"bracketSameLine": true, "bracketSameLine": true,
"arrowParens": "avoid", "arrowParens": "avoid",
"bracketSpacing": false, "bracketSpacing": false
"pluginSearchDirs": false
} }
+20 -20
View File
@@ -1,32 +1,32 @@
import globals from "globals"; import globals from "globals"
import js from "@eslint/js"; import js from "@eslint/js"
import tsEslint from "typescript-eslint"; import tsEslint from "typescript-eslint"
import pluginReact from "eslint-plugin-react"; import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended"
import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended";
export default tsEslint.config( export default tsEslint.config(
{ {
ignores: [ ignores: ["node_modules", "!.*", "**/dist", "**/build", "docs"],
"node_modules",
"!.*",
"**/dist",
"**/build",
],
}, },
{ {
extends: [js.configs.recommended, ...tsEslint.configs.recommended], extends: [js.configs.recommended, ...tsEslint.configs.recommended],
files: ["**/*.{js,mjs,cjs,ts,jsx,tsx}"], files: ["**/*.{js,mjs,cjs,ts,jsx,tsx}"],
languageOptions: { globals: globals.node }, languageOptions: {globals: globals.node},
rules: { rules: {
"no-useless-escape": "off",
"@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/ban-ts-comment": "off", "@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-unused-vars": ["error", { "@typescript-eslint/no-unused-vars": [
"vars": "all", "error",
"args": "after-used", {
"caughtErrors": "none", vars: "all",
"argsIgnorePattern": "^_", args: "none",
}] caughtErrors: "none",
} argsIgnorePattern: "^_",
varsIgnorePattern: "^_",
ignoreRestSiblings: true,
},
],
},
}, },
eslintPluginPrettierRecommended, eslintPluginPrettierRecommended,
); )
+1 -1
View File
@@ -8,6 +8,7 @@
"clean": "pnpm run -r clean", "clean": "pnpm run -r clean",
"build": "pnpm run -r build", "build": "pnpm run -r build",
"link": "for p in $(ls packages); do cd packages/$p; pnpm link --global; cd ../..; done", "link": "for p in $(ls packages); do cd packages/$p; pnpm link --global; cd ../..; done",
"format": "eslint --fix .",
"lint": "eslint .", "lint": "eslint .",
"test": "vitest", "test": "vitest",
"docs:dev": "vitepress dev docs", "docs:dev": "vitepress dev docs",
@@ -19,7 +20,6 @@
"eslint": "~9.23.0", "eslint": "~9.23.0",
"eslint-config-prettier": "^10.1.1", "eslint-config-prettier": "^10.1.1",
"eslint-plugin-prettier": "~5.2.5", "eslint-plugin-prettier": "~5.2.5",
"eslint-plugin-react": "~7.37.4",
"fake-indexeddb": "^6.0.0", "fake-indexeddb": "^6.0.0",
"globals": "~16.0.0", "globals": "~16.0.0",
"happy-dom": "^17.4.4", "happy-dom": "^17.4.4",
+6 -10
View File
@@ -1,10 +1,7 @@
import {now} from "@welshman/lib" import {PublishStatus} from "@welshman/net"
import {PublishStatus, MockAdapter} from "@welshman/net" import {NOTE, makeEvent} from "@welshman/util"
import {NOTE, makeEvent} from "@welshman/util"
import {Nip01Signer} from "@welshman/signer"
import {LOCAL_RELAY_URL} from "@welshman/relay" import {LOCAL_RELAY_URL} from "@welshman/relay"
import {getPubkey, makeSecret} from "@welshman/signer" import {getPubkey, makeSecret} from "@welshman/signer"
import {EventEmitter} from "events"
import {afterEach, beforeEach, describe, expect, it, vi} from "vitest" import {afterEach, beforeEach, describe, expect, it, vi} from "vitest"
import {repository, tracker} from "../src/core" import {repository, tracker} from "../src/core"
import {addSession, dropSession} from "../src/session" import {addSession, dropSession} from "../src/session"
@@ -31,7 +28,7 @@ const mockRequest = {
describe("thunk", () => { describe("thunk", () => {
beforeEach(() => { beforeEach(() => {
vi.useFakeTimers() vi.useFakeTimers()
addSession({method: 'nip01', secret, pubkey}) addSession({method: "nip01", secret, pubkey})
}) })
afterEach(async () => { afterEach(async () => {
@@ -70,7 +67,7 @@ describe("thunk", () => {
describe("publishThunk", () => { describe("publishThunk", () => {
it("should create and publish a thunk", async () => { it("should create and publish a thunk", async () => {
const publishSpy = vi.spyOn(repository, 'publish') const publishSpy = vi.spyOn(repository, "publish")
const result = publishThunk(mockRequest) const result = publishThunk(mockRequest)
expect(publishSpy).toHaveBeenCalled() expect(publishSpy).toHaveBeenCalled()
@@ -79,7 +76,7 @@ describe("thunk", () => {
}) })
it("should handle abort", () => { it("should handle abort", () => {
const removeEventSpy = vi.spyOn(repository, 'removeEvent') const removeEventSpy = vi.spyOn(repository, "removeEvent")
const thunk = publishThunk(mockRequest) const thunk = publishThunk(mockRequest)
thunk.controller.abort() thunk.controller.abort()
@@ -109,8 +106,7 @@ describe("thunk", () => {
}) })
it("should update status during publishing", async () => { it("should update status during publishing", async () => {
const send = vi.fn() const track = vi.spyOn(tracker, "track")
const track = vi.spyOn(tracker, 'track')
const thunk = makeThunk(mockRequest) const thunk = makeThunk(mockRequest)
let status: Record<string, any> = {} let status: Record<string, any> = {}
+23 -15
View File
@@ -1,17 +1,25 @@
import {get, derived} from 'svelte/store' import {derived} from "svelte/store"
import {batch, fromPairs} from '@welshman/lib' import {batch, fromPairs} from "@welshman/lib"
import {PROFILE, FOLLOWS, MUTES, RELAYS, INBOX_RELAYS, getPubkeyTagValues, getListTags} from '@welshman/util' import {
import {throttled, withGetter} from '@welshman/store' PROFILE,
import {RepositoryUpdate} from '@welshman/relay' FOLLOWS,
import {getAll, bulkPut, bulkDelete} from './storage.js' MUTES,
import {relays} from './relays.js' RELAYS,
import {handles, onHandle} from './handles.js' INBOX_RELAYS,
import {zappers, onZapper} from './zappers.js' getPubkeyTagValues,
import {plaintext} from './plaintext.js' getListTags,
import {freshness} from './freshness.js' } from "@welshman/util"
import {repository} from './core.js' import {throttled, withGetter} from "@welshman/store"
import {sessions} from './session.js' import {RepositoryUpdate} from "@welshman/relay"
import {userFollows} from './user.js' import {getAll, bulkPut, bulkDelete} from "./storage.js"
import {relays} from "./relays.js"
import {handles, onHandle} from "./handles.js"
import {zappers, onZapper} from "./zappers.js"
import {plaintext} from "./plaintext.js"
import {freshness} from "./freshness.js"
import {repository} from "./core.js"
import {sessions} from "./session.js"
import {userFollows} from "./user.js"
export const defaultStorageAdapters = { export const defaultStorageAdapters = {
relays: { relays: {
@@ -70,7 +78,7 @@ export const defaultStorageAdapters = {
init: async () => repository.load(await getAll("events")), init: async () => repository.load(await getAll("events")),
sync: () => { sync: () => {
const userFollowPubkeys = withGetter( const userFollowPubkeys = withGetter(
derived(userFollows, l => new Set(getPubkeyTagValues(getListTags(l)))) derived(userFollows, l => new Set(getPubkeyTagValues(getListTags(l)))),
) )
const onUpdate = async ({added, removed}: RepositoryUpdate) => { const onUpdate = async ({added, removed}: RepositoryUpdate) => {
+3 -1
View File
@@ -33,7 +33,9 @@ export const requestDVM = async ({kind, onEvent, ...request}: DVMOpts) => {
const tags = request.tags || [] const tags = request.tags || []
const $signer = signer.get() || new Nip01Signer(makeSecret()) const $signer = signer.get() || new Nip01Signer(makeSecret())
const pubkey = await $signer.getPubkey() const pubkey = await $signer.getPubkey()
const relays = request.relays || Router.get().FromPubkeys(getPubkeyTagValues(tags)).policy(addMinimalFallbacks).getUrls() const relays =
request.relays ||
Router.get().FromPubkeys(getPubkeyTagValues(tags)).policy(addMinimalFallbacks).getUrls()
if (!tags.some(nthEq(0, "expiration"))) { if (!tags.some(nthEq(0, "expiration"))) {
tags.push(["expiration", String(now() + 60)]) tags.push(["expiration", String(now() + 60)])
-2
View File
@@ -1,9 +1,7 @@
import {FOLLOWS, asDecryptedEvent, readList} from "@welshman/util" import {FOLLOWS, asDecryptedEvent, readList} from "@welshman/util"
import {TrustedEvent, PublishedList} from "@welshman/util" import {TrustedEvent, PublishedList} from "@welshman/util"
import {MultiRequestOptions, load} from "@welshman/net"
import {deriveEventsMapped} from "@welshman/store" import {deriveEventsMapped} from "@welshman/store"
import {repository} from "./core.js" import {repository} from "./core.js"
import {Router} from "./router.js"
import {collection} from "./collection.js" import {collection} from "./collection.js"
import {loadWithAsapMetaRelayUrls} from "./relaySelections.js" import {loadWithAsapMetaRelayUrls} from "./relaySelections.js"
-1
View File
@@ -1,5 +1,4 @@
import {writable, derived} from "svelte/store" import {writable, derived} from "svelte/store"
import {MultiRequestOptions} from "@welshman/net"
import {tryCatch, fetchJson, uniq, batcher, postJson, last} from "@welshman/lib" import {tryCatch, fetchJson, uniq, batcher, postJson, last} from "@welshman/lib"
import {collection} from "./collection.js" import {collection} from "./collection.js"
import {deriveProfile} from "./profiles.js" import {deriveProfile} from "./profiles.js"
-2
View File
@@ -1,9 +1,7 @@
import {MUTES, asDecryptedEvent, readList} from "@welshman/util" import {MUTES, asDecryptedEvent, readList} from "@welshman/util"
import {TrustedEvent, PublishedList} from "@welshman/util" import {TrustedEvent, PublishedList} from "@welshman/util"
import {load, MultiRequestOptions} from "@welshman/net"
import {deriveEventsMapped} from "@welshman/store" import {deriveEventsMapped} from "@welshman/store"
import {repository} from "./core.js" import {repository} from "./core.js"
import {Router} from "./router.js"
import {collection} from "./collection.js" import {collection} from "./collection.js"
import {ensurePlaintext} from "./plaintext.js" import {ensurePlaintext} from "./plaintext.js"
import {loadWithAsapMetaRelayUrls} from "./relaySelections.js" import {loadWithAsapMetaRelayUrls} from "./relaySelections.js"
-2
View File
@@ -1,9 +1,7 @@
import {PINS, asDecryptedEvent, readList} from "@welshman/util" import {PINS, asDecryptedEvent, readList} from "@welshman/util"
import {TrustedEvent, PublishedList} from "@welshman/util" import {TrustedEvent, PublishedList} from "@welshman/util"
import {load, MultiRequestOptions} from "@welshman/net"
import {deriveEventsMapped} from "@welshman/store" import {deriveEventsMapped} from "@welshman/store"
import {repository} from "./core.js" import {repository} from "./core.js"
import {Router} from "./router.js"
import {collection} from "./collection.js" import {collection} from "./collection.js"
import {loadWithAsapMetaRelayUrls} from "./relaySelections.js" import {loadWithAsapMetaRelayUrls} from "./relaySelections.js"
+1 -3
View File
@@ -1,10 +1,8 @@
import {derived, readable} from "svelte/store" import {derived, readable} from "svelte/store"
import {readProfile, displayProfile, displayPubkey, PROFILE} from "@welshman/util" import {readProfile, displayProfile, displayPubkey, PROFILE} from "@welshman/util"
import {load, MultiRequestOptions} from "@welshman/net"
import {PublishedProfile} from "@welshman/util" import {PublishedProfile} from "@welshman/util"
import {deriveEventsMapped, withGetter} from "@welshman/store" import {deriveEventsMapped, withGetter} from "@welshman/store"
import {repository} from "./core.js" import {repository} from "./core.js"
import {Router} from "./router.js"
import {collection} from "./collection.js" import {collection} from "./collection.js"
import {loadWithAsapMetaRelayUrls} from "./relaySelections.js" import {loadWithAsapMetaRelayUrls} from "./relaySelections.js"
@@ -25,7 +23,7 @@ export const {
store: profiles, store: profiles,
getKey: profile => profile.event.pubkey, getKey: profile => profile.event.pubkey,
load: (pubkey: string, relays: string[]) => load: (pubkey: string, relays: string[]) =>
loadWithAsapMetaRelayUrls(pubkey, relays, [{kinds: [PROFILE], authors: [pubkey]}]) loadWithAsapMetaRelayUrls(pubkey, relays, [{kinds: [PROFILE], authors: [pubkey]}]),
}) })
export const displayProfileByPubkey = (pubkey: string | undefined) => export const displayProfileByPubkey = (pubkey: string | undefined) =>
+11 -7
View File
@@ -10,10 +10,10 @@ import {
getRelayTagValues, getRelayTagValues,
} from "@welshman/util" } from "@welshman/util"
import {TrustedEvent, Filter, PublishedList, List} from "@welshman/util" import {TrustedEvent, Filter, PublishedList, List} from "@welshman/util"
import {load, MultiRequestOptions} from "@welshman/net" import {load} from "@welshman/net"
import {deriveEventsMapped} from "@welshman/store" import {deriveEventsMapped} from "@welshman/store"
import {repository} from "./core.js" import {repository} from "./core.js"
import {Router, addNoFallbacks} from "./router.js" import {Router} from "./router.js"
import {collection} from "./collection.js" import {collection} from "./collection.js"
export const getRelayUrls = (list?: List): string[] => export const getRelayUrls = (list?: List): string[] =>
@@ -51,13 +51,15 @@ export const {
const router = Router.get() const router = Router.get()
await load({ await load({
relays: router.merge([router.Index(), router.FromRelays(relays), router.FromPubkey(pubkey)]).getUrls(), relays: router
.merge([router.Index(), router.FromRelays(relays), router.FromPubkey(pubkey)])
.getUrls(),
filters: [{kinds: [RELAYS], authors: [pubkey]}], filters: [{kinds: [RELAYS], authors: [pubkey]}],
}) })
}, },
}) })
export const loadWithAsapMetaRelayUrls = <T>(pubkey: string, relays: string[], filters: Filter[]) => { export const loadWithAsapMetaRelayUrls = (pubkey: string, relays: string[], filters: Filter[]) => {
const router = Router.get() const router = Router.get()
return new Promise(resolve => { return new Promise(resolve => {
@@ -69,8 +71,10 @@ export const loadWithAsapMetaRelayUrls = <T>(pubkey: string, relays: string[], f
} }
} }
load({filters, relays: router.merge([router.Index(), router.FromRelays(relays)]).getUrls()}) load({
.then(onLoad) filters,
relays: router.merge([router.Index(), router.FromRelays(relays)]).getUrls(),
}).then(onLoad)
loadRelaySelections(pubkey, relays) loadRelaySelections(pubkey, relays)
.then(() => load({filters, relays: router.FromPubkey(pubkey).getUrls()})) .then(() => load({filters, relays: router.FromPubkey(pubkey).getUrls()}))
@@ -93,5 +97,5 @@ export const {
store: inboxRelaySelections, store: inboxRelaySelections,
getKey: inboxRelaySelections => inboxRelaySelections.event.pubkey, getKey: inboxRelaySelections => inboxRelaySelections.event.pubkey,
load: (pubkey: string, relays: string[]) => load: (pubkey: string, relays: string[]) =>
loadWithAsapMetaRelayUrls(pubkey, relays, [{kinds: [INBOX_RELAYS], authors: [pubkey]}]) loadWithAsapMetaRelayUrls(pubkey, relays, [{kinds: [INBOX_RELAYS], authors: [pubkey]}]),
}) })
+5 -3
View File
@@ -14,7 +14,6 @@ import {
MINUTE, MINUTE,
HOUR, HOUR,
DAY, DAY,
WEEK,
} from "@welshman/lib" } from "@welshman/lib"
import { import {
getFilterId, getFilterId,
@@ -242,7 +241,8 @@ export class Router {
FromPubkey = (pubkey: string) => this.FromRelays(this.getRelaysForPubkey(pubkey, RelayMode.Write)) FromPubkey = (pubkey: string) => this.FromRelays(this.getRelaysForPubkey(pubkey, RelayMode.Write))
PubkeyInbox = (pubkey: string) => this.FromRelays(this.getRelaysForPubkey(pubkey, RelayMode.Inbox)) PubkeyInbox = (pubkey: string) =>
this.FromRelays(this.getRelaysForPubkey(pubkey, RelayMode.Inbox))
ForPubkeys = (pubkeys: string[]) => this.merge(pubkeys.map(pubkey => this.ForPubkey(pubkey))) ForPubkeys = (pubkeys: string[]) => this.merge(pubkeys.map(pubkey => this.ForPubkey(pubkey)))
@@ -493,7 +493,9 @@ export const getFilterSelections = (
const result = [] const result = []
for (const [id, filter] of filtersById.entries()) { for (const [id, filter] of filtersById.entries()) {
const scenario = Router.get().merge(scenariosById.get(id) || []).policy(addMinimalFallbacks) const scenario = Router.get()
.merge(scenariosById.get(id) || [])
.policy(addMinimalFallbacks)
result.push({filters: [filter], relays: scenario.getUrls()}) result.push({filters: [filter], relays: scenario.getUrls()})
} }
+3 -6
View File
@@ -1,12 +1,9 @@
import {openDB, deleteDB} from "idb" import {openDB, deleteDB} from "idb"
import {IDBPDatabase} from "idb" import {IDBPDatabase} from "idb"
import {writable} from "svelte/store" import {writable} from "svelte/store"
import {Unsubscriber, Writable} from "svelte/store" import {Unsubscriber} from "svelte/store"
import {indexBy, call, equals, throttle, fromPairs} from "@welshman/lib" import {call} from "@welshman/lib"
import {TrustedEvent} from "@welshman/util" import {withGetter} from "@welshman/store"
import {Repository} from "@welshman/relay"
import {Tracker} from "@welshman/net"
import {withGetter, adapter, throttled, custom} from "@welshman/store"
export type StorageAdapterOptions = { export type StorageAdapterOptions = {
throttle?: number throttle?: number
+16 -4
View File
@@ -1,5 +1,15 @@
import {Writable, Readable, writable, derived, get} from "svelte/store" import {Writable, Readable, writable, derived, get} from "svelte/store"
import {Deferred, fromPairs, TaskQueue, dissoc, identity, uniq, defer, sleep, assoc} from "@welshman/lib" import {
Deferred,
fromPairs,
TaskQueue,
dissoc,
identity,
uniq,
defer,
sleep,
assoc,
} from "@welshman/lib"
import {stamp, own, hash} from "@welshman/signer" import {stamp, own, hash} from "@welshman/signer"
import { import {
TrustedEvent, TrustedEvent,
@@ -225,9 +235,11 @@ export const thunkQueue = new TaskQueue<Thunk>({
// Update status to pending // Update status to pending
thunk.status.set( thunk.status.set(
fromPairs( fromPairs(
thunk.request.relays thunk.request.relays.map(url => [
.map(url => [url, {status: PublishStatus.Pending, message: "Sending your message..."}]) url,
) {status: PublishStatus.Pending, message: "Sending your message..."},
]),
),
) )
// Send it off // Send it off
-1
View File
@@ -1,6 +1,5 @@
import {writable, derived} from "svelte/store" import {writable, derived} from "svelte/store"
import {Zapper} from "@welshman/util" import {Zapper} from "@welshman/util"
import {MultiRequestOptions} from "@welshman/net"
import { import {
identity, identity,
fetchJson, fetchJson,
+1 -3
View File
@@ -8,9 +8,7 @@ describe("Content Parsing", () => {
describe("Basic Parsing", () => { describe("Basic Parsing", () => {
it("should parse plain text", () => { it("should parse plain text", () => {
const result = parse({content: "Hello world"}) const result = parse({content: "Hello world"})
expect(result).toEqual([ expect(result).toEqual([{type: ParsedType.Text, value: "Hello world", raw: "Hello world"}])
{type: ParsedType.Text, value: "Hello world", raw: "Hello world"},
])
}) })
it("should parse newlines", () => { it("should parse newlines", () => {
+2 -2
View File
@@ -1,5 +1,5 @@
export * from "./nodeviews/index.js" export * from "./nodeviews/index.js"
export * from "./extensions/index.js" export * from "./extensions/index.js"
export * from "./plugins/index.js" export * from "./plugins/index.js"
export {Editor, NodeViewProps} from '@tiptap/core' export {Editor, NodeViewProps} from "@tiptap/core"
export {UploadTask} from 'nostr-editor' export {UploadTask} from "nostr-editor"
+3 -2
View File
@@ -1,6 +1,6 @@
import {bech32, utf8} from "@scure/base" import {bech32, utf8} from "@scure/base"
type Obj<T = any> = Record<string, T>; type Obj<T = any> = Record<string, T>
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// Basic functional programming utilities // Basic functional programming utilities
@@ -134,7 +134,8 @@ export const gt = (x: number | undefined, y: number | undefined) => num(x) > num
export const gte = (x: number | undefined, y: number | undefined) => num(x) >= num(y) export const gte = (x: number | undefined, y: number | undefined) => num(x) >= num(y)
/** Returns maximum value in array, handling undefined values */ /** Returns maximum value in array, handling undefined values */
export const max = (xs: (number | undefined)[]) => xs.reduce((a: number, b) => Math.max(num(a), num(b)), 0) export const max = (xs: (number | undefined)[]) =>
xs.reduce((a: number, b) => Math.max(num(a), num(b)), 0)
/** Returns minimum value in array, handling undefined values */ /** Returns minimum value in array, handling undefined values */
export const min = (xs: (number | undefined)[]) => { export const min = (xs: (number | undefined)[]) => {
+261 -253
View File
@@ -1,12 +1,13 @@
// Copied from https://github.com/sindresorhus/normalize-url // Copied from https://github.com/sindresorhus/normalize-url
/* eslint-disable */
export type Options = { export type Options = {
/** /**
@default 'http' @default 'http'
*/ */
readonly defaultProtocol?: 'https' | 'http'; readonly defaultProtocol?: "https" | "http"
/** /**
Prepends `defaultProtocol` to the URL if it's protocol-relative. Prepends `defaultProtocol` to the URL if it's protocol-relative.
@default true @default true
@@ -20,9 +21,9 @@ export type Options = {
//=> '//sindresorhus.com' //=> '//sindresorhus.com'
``` ```
*/ */
readonly normalizeProtocol?: boolean; readonly normalizeProtocol?: boolean
/** /**
Normalizes HTTPS URLs to HTTP. Normalizes HTTPS URLs to HTTP.
@default false @default false
@@ -36,9 +37,9 @@ export type Options = {
//=> 'http://sindresorhus.com' //=> 'http://sindresorhus.com'
``` ```
*/ */
readonly forceHttp?: boolean; readonly forceHttp?: boolean
/** /**
Normalizes HTTP URLs to HTTPS. Normalizes HTTP URLs to HTTPS.
This option cannot be used with the `forceHttp` option at the same time. This option cannot be used with the `forceHttp` option at the same time.
@@ -54,9 +55,9 @@ export type Options = {
//=> 'https://sindresorhus.com' //=> 'https://sindresorhus.com'
``` ```
*/ */
readonly forceHttps?: boolean; readonly forceHttps?: boolean
/** /**
Strip the [authentication](https://en.wikipedia.org/wiki/Basic_access_authentication) part of a URL. Strip the [authentication](https://en.wikipedia.org/wiki/Basic_access_authentication) part of a URL.
@default true @default true
@@ -70,9 +71,9 @@ export type Options = {
//=> 'https://user:password@sindresorhus.com' //=> 'https://user:password@sindresorhus.com'
``` ```
*/ */
readonly stripAuthentication?: boolean; readonly stripAuthentication?: boolean
/** /**
Removes hash from the URL. Removes hash from the URL.
@default false @default false
@@ -86,9 +87,9 @@ export type Options = {
//=> 'http://sindresorhus.com/about.html' //=> 'http://sindresorhus.com/about.html'
``` ```
*/ */
readonly stripHash?: boolean; readonly stripHash?: boolean
/** /**
Remove the protocol from the URL: `http://sindresorhus.com` → `sindresorhus.com`. Remove the protocol from the URL: `http://sindresorhus.com` → `sindresorhus.com`.
It will only remove `https://` and `http://` protocols. It will only remove `https://` and `http://` protocols.
@@ -104,9 +105,9 @@ export type Options = {
//=> 'sindresorhus.com' //=> 'sindresorhus.com'
``` ```
*/ */
readonly stripProtocol?: boolean; readonly stripProtocol?: boolean
/** /**
Strip the [text fragment](https://web.dev/text-fragments/) part of the URL Strip the [text fragment](https://web.dev/text-fragments/) part of the URL
__Note:__ The text fragment will always be removed if the `stripHash` option is set to `true`, as the hash contains the text fragment. __Note:__ The text fragment will always be removed if the `stripHash` option is set to `true`, as the hash contains the text fragment.
@@ -128,9 +129,9 @@ export type Options = {
//=> 'http://sindresorhus.com/about.html#section:~:text=hello' //=> 'http://sindresorhus.com/about.html#section:~:text=hello'
``` ```
*/ */
readonly stripTextFragment?: boolean; readonly stripTextFragment?: boolean
/** /**
Removes `www.` from the URL. Removes `www.` from the URL.
@default true @default true
@@ -144,9 +145,9 @@ export type Options = {
//=> 'http://www.sindresorhus.com' //=> 'http://www.sindresorhus.com'
``` ```
*/ */
readonly stripWWW?: boolean; readonly stripWWW?: boolean
/** /**
Removes query parameters that matches any of the provided strings or regexes. Removes query parameters that matches any of the provided strings or regexes.
@default [/^utm_\w+/i] @default [/^utm_\w+/i]
@@ -177,9 +178,9 @@ export type Options = {
//=> 'http://www.sindresorhus.com/?foo=bar&ref=test_ref&utm_medium=test' //=> 'http://www.sindresorhus.com/?foo=bar&ref=test_ref&utm_medium=test'
``` ```
*/ */
readonly removeQueryParameters?: ReadonlyArray<RegExp | string> | boolean; readonly removeQueryParameters?: ReadonlyArray<RegExp | string> | boolean
/** /**
Keeps only query parameters that matches any of the provided strings or regexes. Keeps only query parameters that matches any of the provided strings or regexes.
__Note__: It overrides the `removeQueryParameters` option. __Note__: It overrides the `removeQueryParameters` option.
@@ -194,9 +195,9 @@ export type Options = {
//=> 'https://sindresorhus.com/?ref=unicorn' //=> 'https://sindresorhus.com/?ref=unicorn'
``` ```
*/ */
readonly keepQueryParameters?: ReadonlyArray<RegExp | string>; readonly keepQueryParameters?: ReadonlyArray<RegExp | string>
/** /**
Removes trailing slash. Removes trailing slash.
__Note__: Trailing slash is always removed if the URL doesn't have a pathname unless the `removeSingleSlash` option is set to `false`. __Note__: Trailing slash is always removed if the URL doesn't have a pathname unless the `removeSingleSlash` option is set to `false`.
@@ -215,9 +216,9 @@ export type Options = {
//=> 'http://sindresorhus.com' //=> 'http://sindresorhus.com'
``` ```
*/ */
readonly removeTrailingSlash?: boolean; readonly removeTrailingSlash?: boolean
/** /**
Remove a sole `/` pathname in the output. This option is independent of `removeTrailingSlash`. Remove a sole `/` pathname in the output. This option is independent of `removeTrailingSlash`.
@default true @default true
@@ -231,9 +232,9 @@ export type Options = {
//=> 'https://sindresorhus.com/' //=> 'https://sindresorhus.com/'
``` ```
*/ */
readonly removeSingleSlash?: boolean; readonly removeSingleSlash?: boolean
/** /**
Removes the default directory index file from path that matches any of the provided strings or regexes. Removes the default directory index file from path that matches any of the provided strings or regexes.
When `true`, the regex `/^index\.[a-z]+$/` is used. When `true`, the regex `/^index\.[a-z]+$/` is used.
@@ -247,9 +248,9 @@ export type Options = {
//=> 'http://sindresorhus.com/foo' //=> 'http://sindresorhus.com/foo'
``` ```
*/ */
readonly removeDirectoryIndex?: boolean | ReadonlyArray<RegExp | string>; readonly removeDirectoryIndex?: boolean | ReadonlyArray<RegExp | string>
/** /**
Removes an explicit port number from the URL. Removes an explicit port number from the URL.
Port 443 is always removed from HTTPS URLs and 80 is always removed from HTTP URLs regardless of this option. Port 443 is always removed from HTTPS URLs and 80 is always removed from HTTP URLs regardless of this option.
@@ -264,9 +265,9 @@ export type Options = {
//=> 'http://sindresorhus.com' //=> 'http://sindresorhus.com'
``` ```
*/ */
readonly removeExplicitPort?: boolean; readonly removeExplicitPort?: boolean
/** /**
Sorts the query parameters alphabetically by key. Sorts the query parameters alphabetically by key.
@default true @default true
@@ -279,79 +280,74 @@ export type Options = {
//=> 'http://sindresorhus.com/?b=two&a=one&c=three' //=> 'http://sindresorhus.com/?b=two&a=one&c=three'
``` ```
*/ */
readonly sortQueryParameters?: boolean; readonly sortQueryParameters?: boolean
}; }
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs // https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs
const DATA_URL_DEFAULT_MIME_TYPE = 'text/plain' const DATA_URL_DEFAULT_MIME_TYPE = "text/plain"
const DATA_URL_DEFAULT_CHARSET = 'us-ascii' const DATA_URL_DEFAULT_CHARSET = "us-ascii"
const testParameter = (name: string, filters: any[]) => filters.some(filter => filter instanceof RegExp ? filter.test(name) : filter === name) const testParameter = (name: string, filters: any[]) =>
filters.some(filter => (filter instanceof RegExp ? filter.test(name) : filter === name))
const supportedProtocols = new Set([ const supportedProtocols = new Set(["https:", "http:", "file:"])
'https:',
'http:',
'file:',
])
const hasCustomProtocol = (urlString: string) => { const hasCustomProtocol = (urlString: string) => {
try { try {
const {protocol} = new URL(urlString) const {protocol} = new URL(urlString)
return protocol.endsWith(':') && !supportedProtocols.has(protocol) return protocol.endsWith(":") && !supportedProtocols.has(protocol)
} catch { } catch {
return false return false
} }
} }
const normalizeDataURL = (urlString: string, {stripHash}: {stripHash: boolean}) => { const normalizeDataURL = (urlString: string, {stripHash}: {stripHash: boolean}) => {
const match = /^data:(?<type>[^,]*?),(?<data>[^#]*?)(?:#(?<hash>.*))?$/.exec(urlString) const match = /^data:(?<type>[^,]*?),(?<data>[^#]*?)(?:#(?<hash>.*))?$/.exec(urlString)
if (!match) { if (!match) {
throw new Error(`Invalid URL: ${urlString}`) throw new Error(`Invalid URL: ${urlString}`)
} }
let {type, data, hash} = match.groups as any let {type, data, hash} = match.groups as any
const mediaType = type.split(';') const mediaType = type.split(";")
hash = stripHash ? '' : hash hash = stripHash ? "" : hash
let isBase64 = false let isBase64 = false
if (mediaType[mediaType.length - 1] === 'base64') { if (mediaType[mediaType.length - 1] === "base64") {
mediaType.pop() mediaType.pop()
isBase64 = true isBase64 = true
} }
// Lowercase MIME type // Lowercase MIME type
const mimeType = mediaType.shift()?.toLowerCase() ?? '' const mimeType = mediaType.shift()?.toLowerCase() ?? ""
const attributes = mediaType const attributes = mediaType
.map((attribute: string) => { .map((attribute: string) => {
let [key, value = ''] = attribute.split('=').map((s: string) => s.trim()) let [key, value = ""] = attribute.split("=").map((s: string) => s.trim())
// Lowercase `charset` // Lowercase `charset`
if (key === 'charset') { if (key === "charset") {
value = value.toLowerCase() value = value.toLowerCase()
if (value === DATA_URL_DEFAULT_CHARSET) { if (value === DATA_URL_DEFAULT_CHARSET) {
return '' return ""
} }
} }
return `${key}${value ? `=${value}` : ''}` return `${key}${value ? `=${value}` : ""}`
}) })
.filter(Boolean) .filter(Boolean)
const normalizedMediaType = [ const normalizedMediaType = [...attributes]
...attributes,
]
if (isBase64) { if (isBase64) {
normalizedMediaType.push('base64') normalizedMediaType.push("base64")
} }
if (normalizedMediaType.length > 0 || (mimeType && mimeType !== DATA_URL_DEFAULT_MIME_TYPE)) { if (normalizedMediaType.length > 0 || (mimeType && mimeType !== DATA_URL_DEFAULT_MIME_TYPE)) {
normalizedMediaType.unshift(mimeType) normalizedMediaType.unshift(mimeType)
} }
return `data:${normalizedMediaType.join(';')},${isBase64 ? data.trim() : data}${hash ? `#${hash}` : ''}` return `data:${normalizedMediaType.join(";")},${isBase64 ? data.trim() : data}${hash ? `#${hash}` : ""}`
} }
/** /**
@@ -374,212 +370,224 @@ normalizeUrl('//www.sindresorhus.com:80/../baz?b=bar&a=foo');
*/ */
export default function normalizeUrl(urlString: string, opts?: Options): string { export default function normalizeUrl(urlString: string, opts?: Options): string {
const options = { const options = {
defaultProtocol: 'http', defaultProtocol: "http",
normalizeProtocol: true, normalizeProtocol: true,
forceHttp: false, forceHttp: false,
forceHttps: false, forceHttps: false,
stripAuthentication: true, stripAuthentication: true,
stripHash: false, stripHash: false,
stripTextFragment: true, stripTextFragment: true,
stripWWW: true, stripWWW: true,
removeQueryParameters: [/^utm_\w+/i], removeQueryParameters: [/^utm_\w+/i],
removeTrailingSlash: true, removeTrailingSlash: true,
removeSingleSlash: true, removeSingleSlash: true,
removeDirectoryIndex: false, removeDirectoryIndex: false,
removeExplicitPort: false, removeExplicitPort: false,
sortQueryParameters: true, sortQueryParameters: true,
...opts, ...opts,
} }
// Legacy: Append `:` to the protocol if missing. // Legacy: Append `:` to the protocol if missing.
if (typeof options.defaultProtocol === 'string' && !options.defaultProtocol.endsWith(':')) { if (typeof options.defaultProtocol === "string" && !options.defaultProtocol.endsWith(":")) {
options.defaultProtocol = `${options.defaultProtocol}:` options.defaultProtocol = `${options.defaultProtocol}:`
} }
urlString = urlString.trim() urlString = urlString.trim()
// Data URL // Data URL
if (/^data:/i.test(urlString)) { if (/^data:/i.test(urlString)) {
return normalizeDataURL(urlString, options) return normalizeDataURL(urlString, options)
} }
if (hasCustomProtocol(urlString)) { if (hasCustomProtocol(urlString)) {
return urlString return urlString
} }
const hasRelativeProtocol = urlString.startsWith('//') const hasRelativeProtocol = urlString.startsWith("//")
const isRelativeUrl = !hasRelativeProtocol && /^\.*\//.test(urlString) const isRelativeUrl = !hasRelativeProtocol && /^\.*\//.test(urlString)
// Prepend protocol // Prepend protocol
if (!isRelativeUrl) { if (!isRelativeUrl) {
urlString = urlString.replace(/^(?!(?:\w+:)?\/\/)|^\/\//, options.defaultProtocol) urlString = urlString.replace(/^(?!(?:\w+:)?\/\/)|^\/\//, options.defaultProtocol)
} }
const urlObject = new URL(urlString) const urlObject = new URL(urlString)
if (options.forceHttp && options.forceHttps) { if (options.forceHttp && options.forceHttps) {
throw new Error('The `forceHttp` and `forceHttps` options cannot be used together') throw new Error("The `forceHttp` and `forceHttps` options cannot be used together")
} }
if (options.forceHttp && urlObject.protocol === 'https:') { if (options.forceHttp && urlObject.protocol === "https:") {
urlObject.protocol = 'http:' urlObject.protocol = "http:"
} }
if (options.forceHttps && urlObject.protocol === 'http:') { if (options.forceHttps && urlObject.protocol === "http:") {
urlObject.protocol = 'https:' urlObject.protocol = "https:"
} }
// Remove auth // Remove auth
if (options.stripAuthentication) { if (options.stripAuthentication) {
urlObject.username = '' urlObject.username = ""
urlObject.password = '' urlObject.password = ""
} }
// Remove hash // Remove hash
if (options.stripHash) { if (options.stripHash) {
urlObject.hash = '' urlObject.hash = ""
} else if (options.stripTextFragment) { } else if (options.stripTextFragment) {
urlObject.hash = urlObject.hash.replace(/#?:~:text.*?$/i, '') urlObject.hash = urlObject.hash.replace(/#?:~:text.*?$/i, "")
} }
// Remove duplicate slashes if not preceded by a protocol // Remove duplicate slashes if not preceded by a protocol
// NOTE: This could be implemented using a single negative lookbehind // NOTE: This could be implemented using a single negative lookbehind
// regex, but we avoid that to maintain compatibility with older js engines // regex, but we avoid that to maintain compatibility with older js engines
// which do not have support for that feature. // which do not have support for that feature.
if (urlObject.pathname) { if (urlObject.pathname) {
// TODO: Replace everything below with `urlObject.pathname = urlObject.pathname.replace(/(?<!\b[a-z][a-z\d+\-.]{1,50}:)\/{2,}/g, '/');` when Safari supports negative lookbehind. // TODO: Replace everything below with `urlObject.pathname = urlObject.pathname.replace(/(?<!\b[a-z][a-z\d+\-.]{1,50}:)\/{2,}/g, '/');` when Safari supports negative lookbehind.
// Split the string by occurrences of this protocol regex, and perform // Split the string by occurrences of this protocol regex, and perform
// duplicate-slash replacement on the strings between those occurrences // duplicate-slash replacement on the strings between those occurrences
// (if any). // (if any).
const protocolRegex = /\b[a-z][a-z\d+\-.]{1,50}:\/\//g const protocolRegex = /\b[a-z][a-z\d+\-.]{1,50}:\/\//g
let lastIndex = 0 let lastIndex = 0
let result = '' let result = ""
for (;;) { for (;;) {
const match = protocolRegex.exec(urlObject.pathname) const match = protocolRegex.exec(urlObject.pathname)
if (!match) { if (!match) {
break break
} }
const protocol = match[0] const protocol = match[0]
const protocolAtIndex = match.index const protocolAtIndex = match.index
const intermediate = urlObject.pathname.slice(lastIndex, protocolAtIndex) const intermediate = urlObject.pathname.slice(lastIndex, protocolAtIndex)
result += intermediate.replace(/\/{2,}/g, '/') result += intermediate.replace(/\/{2,}/g, "/")
result += protocol result += protocol
lastIndex = protocolAtIndex + protocol.length lastIndex = protocolAtIndex + protocol.length
} }
const remnant = urlObject.pathname.slice(lastIndex, urlObject.pathname.length) const remnant = urlObject.pathname.slice(lastIndex, urlObject.pathname.length)
result += remnant.replace(/\/{2,}/g, '/') result += remnant.replace(/\/{2,}/g, "/")
urlObject.pathname = result urlObject.pathname = result
} }
// Decode URI octets // Decode URI octets
if (urlObject.pathname) { if (urlObject.pathname) {
try { try {
urlObject.pathname = decodeURI(urlObject.pathname) urlObject.pathname = decodeURI(urlObject.pathname)
} catch {} } catch {}
} }
// Remove directory index // Remove directory index
if (options.removeDirectoryIndex === true) { if (options.removeDirectoryIndex === true) {
options.removeDirectoryIndex = [/^index\.[a-z]+$/] options.removeDirectoryIndex = [/^index\.[a-z]+$/]
} }
if (Array.isArray(options.removeDirectoryIndex) && options.removeDirectoryIndex.length > 0) { if (Array.isArray(options.removeDirectoryIndex) && options.removeDirectoryIndex.length > 0) {
let pathComponents = urlObject.pathname.split('/') let pathComponents = urlObject.pathname.split("/")
const lastComponent = pathComponents[pathComponents.length - 1] const lastComponent = pathComponents[pathComponents.length - 1]
if (testParameter(lastComponent, options.removeDirectoryIndex)) { if (testParameter(lastComponent, options.removeDirectoryIndex)) {
pathComponents = pathComponents.slice(0, -1) pathComponents = pathComponents.slice(0, -1)
urlObject.pathname = pathComponents.slice(1).join('/') + '/' urlObject.pathname = pathComponents.slice(1).join("/") + "/"
} }
} }
if (urlObject.hostname) { if (urlObject.hostname) {
// Remove trailing dot // Remove trailing dot
urlObject.hostname = urlObject.hostname.replace(/\.$/, '') urlObject.hostname = urlObject.hostname.replace(/\.$/, "")
// Remove `www.` // Remove `www.`
if (options.stripWWW && /^www\.(?!www\.)[a-z\-\d]{1,63}\.[a-z.\-\d]{2,63}$/.test(urlObject.hostname)) { if (
// Each label should be max 63 at length (min: 1). options.stripWWW &&
// Source: https://en.wikipedia.org/wiki/Hostname#Restrictions_on_valid_host_names /^www\.(?!www\.)[a-z\-\d]{1,63}\.[a-z.\-\d]{2,63}$/.test(urlObject.hostname)
// Each TLD should be up to 63 characters long (min: 2). ) {
// It is technically possible to have a single character TLD, but none currently exist. // Each label should be max 63 at length (min: 1).
urlObject.hostname = urlObject.hostname.replace(/^www\./, '') // Source: https://en.wikipedia.org/wiki/Hostname#Restrictions_on_valid_host_names
} // Each TLD should be up to 63 characters long (min: 2).
} // It is technically possible to have a single character TLD, but none currently exist.
urlObject.hostname = urlObject.hostname.replace(/^www\./, "")
}
}
// Remove query unwanted parameters // Remove query unwanted parameters
if (Array.isArray(options.removeQueryParameters)) { if (Array.isArray(options.removeQueryParameters)) {
// @ts-ignore // @ts-ignore
for (const key of [...urlObject.searchParams.keys()]) { for (const key of [...urlObject.searchParams.keys()]) {
if (testParameter(key, options.removeQueryParameters)) { if (testParameter(key, options.removeQueryParameters)) {
urlObject.searchParams.delete(key) urlObject.searchParams.delete(key)
} }
} }
} }
if (!Array.isArray(options.keepQueryParameters) && options.removeQueryParameters === true) { if (!Array.isArray(options.keepQueryParameters) && options.removeQueryParameters === true) {
urlObject.search = '' urlObject.search = ""
} }
// Keep wanted query parameters // Keep wanted query parameters
if (Array.isArray(options.keepQueryParameters) && options.keepQueryParameters.length > 0) { if (Array.isArray(options.keepQueryParameters) && options.keepQueryParameters.length > 0) {
// @ts-ignore // @ts-ignore
for (const key of [...urlObject.searchParams.keys()]) { for (const key of [...urlObject.searchParams.keys()]) {
if (!testParameter(key, options.keepQueryParameters)) { if (!testParameter(key, options.keepQueryParameters)) {
urlObject.searchParams.delete(key) urlObject.searchParams.delete(key)
} }
} }
} }
// Sort query parameters // Sort query parameters
if (options.sortQueryParameters) { if (options.sortQueryParameters) {
urlObject.searchParams.sort() urlObject.searchParams.sort()
// Calling `.sort()` encodes the search parameters, so we need to decode them again. // Calling `.sort()` encodes the search parameters, so we need to decode them again.
try { try {
urlObject.search = decodeURIComponent(urlObject.search) urlObject.search = decodeURIComponent(urlObject.search)
} catch {} } catch {}
} }
if (options.removeTrailingSlash) { if (options.removeTrailingSlash) {
urlObject.pathname = urlObject.pathname.replace(/\/$/, '') urlObject.pathname = urlObject.pathname.replace(/\/$/, "")
} }
// Remove an explicit port number, excluding a default port number, if applicable // Remove an explicit port number, excluding a default port number, if applicable
if (options.removeExplicitPort && urlObject.port) { if (options.removeExplicitPort && urlObject.port) {
urlObject.port = '' urlObject.port = ""
} }
const oldUrlString = urlString const oldUrlString = urlString
// Take advantage of many of the Node `url` normalizations // Take advantage of many of the Node `url` normalizations
urlString = urlObject.toString() urlString = urlObject.toString()
if (!options.removeSingleSlash && urlObject.pathname === '/' && !oldUrlString.endsWith('/') && urlObject.hash === '') { if (
urlString = urlString.replace(/\/$/, '') !options.removeSingleSlash &&
} urlObject.pathname === "/" &&
!oldUrlString.endsWith("/") &&
urlObject.hash === ""
) {
urlString = urlString.replace(/\/$/, "")
}
// Remove ending `/` unless removeSingleSlash is false // Remove ending `/` unless removeSingleSlash is false
if ((options.removeTrailingSlash || urlObject.pathname === '/') && urlObject.hash === '' && options.removeSingleSlash) { if (
urlString = urlString.replace(/\/$/, '') (options.removeTrailingSlash || urlObject.pathname === "/") &&
} urlObject.hash === "" &&
options.removeSingleSlash
) {
urlString = urlString.replace(/\/$/, "")
}
// Restore relative protocol, if applicable // Restore relative protocol, if applicable
if (hasRelativeProtocol && !options.normalizeProtocol) { if (hasRelativeProtocol && !options.normalizeProtocol) {
urlString = urlString.replace(/^http:\/\//, '//') urlString = urlString.replace(/^http:\/\//, "//")
} }
// Remove http/https // Remove http/https
if (options.stripProtocol) { if (options.stripProtocol) {
urlString = urlString.replace(/^(?:https?:)?\/\//, '') urlString = urlString.replace(/^(?:https?:)?\/\//, "")
} }
return urlString return urlString
} }
+22 -20
View File
@@ -1,13 +1,12 @@
import EventEmitter from "events" import EventEmitter from "events"
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest" import {describe, expect, it, vi, beforeEach, afterEach} from "vitest"
import { isRelayUrl } from "@welshman/util" import {LocalRelay, Repository, LOCAL_RELAY_URL} from "@welshman/relay"
import { LocalRelay, Repository, LOCAL_RELAY_URL } from "@welshman/relay" import {AdapterEvent, SocketAdapter, LocalAdapter, getAdapter} from "../src/adapter"
import { AdapterEvent, SocketAdapter, LocalAdapter, getAdapter } from "../src/adapter" import {ClientMessage, RelayMessage} from "../src/message"
import { ClientMessage, RelayMessage } from "../src/message" import {Socket, SocketEvent} from "../src/socket"
import { Socket, SocketEvent } from "../src/socket" import {Pool} from "../src/pool"
import { Pool } from "../src/pool"
vi.mock('isomorphic-ws', () => { vi.mock("isomorphic-ws", () => {
const WebSocket = vi.fn(function (this: any) { const WebSocket = vi.fn(function (this: any) {
setTimeout(() => this.onopen()) setTimeout(() => this.onopen())
}) })
@@ -18,7 +17,7 @@ vi.mock('isomorphic-ws', () => {
this.onclose() this.onclose()
}) })
return { default: WebSocket } return {default: WebSocket}
}) })
describe("SocketAdapter", () => { describe("SocketAdapter", () => {
@@ -27,7 +26,7 @@ describe("SocketAdapter", () => {
beforeEach(() => { beforeEach(() => {
vi.useFakeTimers() vi.useFakeTimers()
socket = new Socket('wss://test.relay') socket = new Socket("wss://test.relay")
adapter = new SocketAdapter(socket) adapter = new SocketAdapter(socket)
}) })
@@ -48,15 +47,15 @@ describe("SocketAdapter", () => {
const receiveSpy = vi.fn() const receiveSpy = vi.fn()
adapter.on(AdapterEvent.Receive, receiveSpy) adapter.on(AdapterEvent.Receive, receiveSpy)
const message: RelayMessage = ["EVENT", "123", { id: "123", kind: 1 }] const message: RelayMessage = ["EVENT", "123", {id: "123", kind: 1}]
socket.emit(SocketEvent.Receive, message, "wss://test.relay") socket.emit(SocketEvent.Receive, message, "wss://test.relay")
expect(receiveSpy).toHaveBeenCalledWith(message, "wss://test.relay") expect(receiveSpy).toHaveBeenCalledWith(message, "wss://test.relay")
}) })
it("should send messages to socket", () => { it("should send messages to socket", () => {
const sendSpy = vi.spyOn(socket, 'send') const sendSpy = vi.spyOn(socket, "send")
const message: ClientMessage = ["EVENT", { id: "123", kind: 1 }] const message: ClientMessage = ["EVENT", {id: "123", kind: 1}]
adapter.send(message) adapter.send(message)
expect(sendSpy).toHaveBeenCalledWith(message) expect(sendSpy).toHaveBeenCalledWith(message)
@@ -77,7 +76,7 @@ describe("LocalAdapter", () => {
const mockRelay = new EventEmitter() const mockRelay = new EventEmitter()
Object.assign(mockRelay, { Object.assign(mockRelay, {
send: vi.fn(), send: vi.fn(),
removeAllListeners: vi.fn() removeAllListeners: vi.fn(),
}) })
relay = mockRelay as unknown as LocalRelay & EventEmitter relay = mockRelay as unknown as LocalRelay & EventEmitter
adapter = new LocalAdapter(relay) adapter = new LocalAdapter(relay)
@@ -98,14 +97,14 @@ describe("LocalAdapter", () => {
const receiveSpy = vi.fn() const receiveSpy = vi.fn()
adapter.on(AdapterEvent.Receive, receiveSpy) adapter.on(AdapterEvent.Receive, receiveSpy)
const message: RelayMessage = ["EVENT", "123", { id: "123", kind: 1 }] const message: RelayMessage = ["EVENT", "123", {id: "123", kind: 1}]
relay.emit("*", ...message) relay.emit("*", ...message)
expect(receiveSpy).toHaveBeenCalledWith(message, LOCAL_RELAY_URL) expect(receiveSpy).toHaveBeenCalledWith(message, LOCAL_RELAY_URL)
}) })
it("should send messages to relay", () => { it("should send messages to relay", () => {
const message: ClientMessage = ["EVENT", { id: "123", kind: 1 }] const message: ClientMessage = ["EVENT", {id: "123", kind: 1}]
adapter.send(message) adapter.send(message)
expect(relay.send).toHaveBeenCalledWith("EVENT", message[1]) expect(relay.send).toHaveBeenCalledWith("EVENT", message[1])
@@ -136,13 +135,13 @@ describe("getAdapter", () => {
it("should return LocalAdapter for local relay URL", () => { it("should return LocalAdapter for local relay URL", () => {
const url = LOCAL_RELAY_URL const url = LOCAL_RELAY_URL
const adapter = getAdapter(url, { repository }) const adapter = getAdapter(url, {repository})
expect(adapter).toBeInstanceOf(LocalAdapter) expect(adapter).toBeInstanceOf(LocalAdapter)
}) })
it("should return SocketAdapter for remote relay URL", () => { it("should return SocketAdapter for remote relay URL", () => {
const url = "wss://test.relay" const url = "wss://test.relay"
const adapter = getAdapter(url, { pool }) const adapter = getAdapter(url, {pool})
expect(adapter).toBeInstanceOf(SocketAdapter) expect(adapter).toBeInstanceOf(SocketAdapter)
}) })
@@ -151,9 +150,12 @@ describe("getAdapter", () => {
const getCustomAdapter = vi.fn().mockReturnValue(customAdapter) const getCustomAdapter = vi.fn().mockReturnValue(customAdapter)
const url = "wss://test.relay" const url = "wss://test.relay"
const adapter = getAdapter(url, { getAdapter: getCustomAdapter }) const adapter = getAdapter(url, {getAdapter: getCustomAdapter})
expect(getCustomAdapter).toHaveBeenCalledWith(url, expect.objectContaining({ getAdapter: getCustomAdapter })) expect(getCustomAdapter).toHaveBeenCalledWith(
url,
expect.objectContaining({getAdapter: getCustomAdapter}),
)
expect(adapter).toBe(customAdapter) expect(adapter).toBe(customAdapter)
}) })
}) })
+14 -15
View File
@@ -1,12 +1,11 @@
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest" import {describe, expect, it, vi, beforeEach, afterEach} from "vitest"
import { Socket, SocketStatus, SocketEvent } from "../src/socket" import {Socket, SocketStatus, SocketEvent} from "../src/socket"
import { makeEvent, StampedEvent, CLIENT_AUTH } from "@welshman/util" import {StampedEvent, CLIENT_AUTH} from "@welshman/util"
import { Nip01Signer } from "@welshman/signer" import {Nip01Signer} from "@welshman/signer"
import { AuthState, AuthStatus, AuthStateEvent, makeAuthEvent } from "../src/auth" import {AuthStatus, AuthStateEvent} from "../src/auth"
import EventEmitter from "events" import {RelayMessage} from "../src/message"
import { RelayMessage } from "../src/message"
vi.mock('isomorphic-ws', () => { vi.mock("isomorphic-ws", () => {
const WebSocket = vi.fn(function (this: any) { const WebSocket = vi.fn(function (this: any) {
setTimeout(() => this.onopen()) setTimeout(() => this.onopen())
}) })
@@ -17,14 +16,14 @@ vi.mock('isomorphic-ws', () => {
this.onclose() this.onclose()
}) })
return { default: WebSocket } return {default: WebSocket}
}) })
describe('auth', () => { describe("auth", () => {
let socket: Socket let socket: Socket
beforeEach(() => { beforeEach(() => {
socket = new Socket('wss://test.relay') socket = new Socket("wss://test.relay")
}) })
afterEach(() => { afterEach(() => {
@@ -72,7 +71,7 @@ describe('auth', () => {
}) })
it("should handle client AUTH message", () => { it("should handle client AUTH message", () => {
const message: RelayMessage = ["AUTH", { id: "123", kind: CLIENT_AUTH }] const message: RelayMessage = ["AUTH", {id: "123", kind: CLIENT_AUTH}]
socket.emit(SocketEvent.Sending, message) socket.emit(SocketEvent.Sending, message)
expect(socket.auth.status).toBe(AuthStatus.PendingResponse) expect(socket.auth.status).toBe(AuthStatus.PendingResponse)
@@ -113,7 +112,7 @@ describe('auth', () => {
const sign = vi.fn() const sign = vi.fn()
await expect(socket.auth.authenticate(sign)).rejects.toThrow( await expect(socket.auth.authenticate(sign)).rejects.toThrow(
"Attempted to authenticate with no challenge" "Attempted to authenticate with no challenge",
) )
}) })
@@ -124,7 +123,7 @@ describe('auth', () => {
socket.auth.status = AuthStatus.PendingResponse socket.auth.status = AuthStatus.PendingResponse
await expect(socket.auth.authenticate(sign)).rejects.toThrow( await expect(socket.auth.authenticate(sign)).rejects.toThrow(
"Attempted to authenticate when auth is already auth:status:pending_response" "Attempted to authenticate when auth is already auth:status:pending_response",
) )
}) })
@@ -140,7 +139,7 @@ describe('auth', () => {
}) })
it("should send AUTH message", async () => { it("should send AUTH message", async () => {
const sendSpy = vi.spyOn(socket, 'send') const sendSpy = vi.spyOn(socket, "send")
let event let event
socket.auth.challenge = "challenge123" socket.auth.challenge = "challenge123"
+58 -49
View File
@@ -1,14 +1,14 @@
import { AUTH_JOIN } from "@welshman/util" import {AUTH_JOIN} from "@welshman/util"
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest" import {describe, expect, it, vi, beforeEach, afterEach} from "vitest"
import { Socket, SocketStatus, SocketEvent } from "../src/socket" import {Socket, SocketStatus, SocketEvent} from "../src/socket"
import { AuthStatus, AuthStateEvent } from "../src/auth" import {AuthStatus, AuthStateEvent} from "../src/auth"
import { import {
socketPolicyAuthBuffer, socketPolicyAuthBuffer,
socketPolicyConnectOnSend, socketPolicyConnectOnSend,
socketPolicyCloseOnTimeout, socketPolicyCloseOnTimeout,
socketPolicyReopenActive socketPolicyReopenActive,
} from "../src/policy" } from "../src/policy"
import { ClientMessage, RelayMessage } from "../src/message" import {ClientMessage, RelayMessage} from "../src/message"
// Hoist mock definition to top level // Hoist mock definition to top level
const mockWs = vi.hoisted(() => ({ const mockWs = vi.hoisted(() => ({
@@ -21,11 +21,11 @@ const mockWs = vi.hoisted(() => ({
})) }))
// Mock the WebSocket module // Mock the WebSocket module
vi.mock('isomorphic-ws', () => ({ vi.mock("isomorphic-ws", () => ({
default: mockWs default: mockWs,
})) }))
describe('policy', () => { describe("policy", () => {
let socket: Socket let socket: Socket
beforeEach(() => { beforeEach(() => {
@@ -42,22 +42,22 @@ describe('policy', () => {
describe("socketPolicyAuthBuffer", () => { describe("socketPolicyAuthBuffer", () => {
it("should buffer messages when not authenticated", () => { it("should buffer messages when not authenticated", () => {
const cleanup = socketPolicyAuthBuffer(socket) const cleanup = socketPolicyAuthBuffer(socket)
const sendSpy = vi.spyOn(socket, 'send') const sendSpy = vi.spyOn(socket, "send")
socket.emit(SocketEvent.Receive, ["AUTH", "challenge"]) socket.emit(SocketEvent.Receive, ["AUTH", "challenge"])
// Regular event should be buffered // Regular event should be buffered
const event: ClientMessage = ["EVENT", { id: "123"}] const event: ClientMessage = ["EVENT", {id: "123"}]
socket.send(event) socket.send(event)
expect(sendSpy).toHaveBeenCalledWith(event) expect(sendSpy).toHaveBeenCalledWith(event)
// Auth event should not be buffered // Auth event should not be buffered
const authEvent: ClientMessage = ["AUTH", { id: "456" }] const authEvent: ClientMessage = ["AUTH", {id: "456"}]
socket.send(authEvent) socket.send(authEvent)
expect(sendSpy).toHaveBeenCalledWith(authEvent) expect(sendSpy).toHaveBeenCalledWith(authEvent)
// Auth join event should not be buffered // Auth join event should not be buffered
const joinEvent: ClientMessage = ["EVENT", { id: "789", kind: AUTH_JOIN }] const joinEvent: ClientMessage = ["EVENT", {id: "789", kind: AUTH_JOIN}]
socket.send(joinEvent) socket.send(joinEvent)
expect(sendSpy).toHaveBeenCalledWith(joinEvent) expect(sendSpy).toHaveBeenCalledWith(joinEvent)
@@ -66,18 +66,18 @@ describe('policy', () => {
it("should send buffered messages when auth succeeds", () => { it("should send buffered messages when auth succeeds", () => {
const cleanup = socketPolicyAuthBuffer(socket) const cleanup = socketPolicyAuthBuffer(socket)
const sendSpy = vi.spyOn(socket, 'send') const sendSpy = vi.spyOn(socket, "send")
socket.emit(SocketEvent.Receive, ["AUTH", "challenge"]) socket.emit(SocketEvent.Receive, ["AUTH", "challenge"])
// Buffer some messages // Buffer some messages
const event1: ClientMessage = ["EVENT", { id: "123"}] const event1: ClientMessage = ["EVENT", {id: "123"}]
const event2: ClientMessage = ["EVENT", { id: "456"}] const event2: ClientMessage = ["EVENT", {id: "456"}]
socket.send(event1) socket.send(event1)
socket.send(event2) socket.send(event2)
// Auth succeeds // Auth succeeds
socket.send(["AUTH", { id: "auth" }]) socket.send(["AUTH", {id: "auth"}])
socket.emit(AuthStateEvent.Status, AuthStatus.Ok) socket.emit(AuthStateEvent.Status, AuthStatus.Ok)
expect(sendSpy).toHaveBeenCalledWith(event1) expect(sendSpy).toHaveBeenCalledWith(event1)
@@ -88,12 +88,12 @@ describe('policy', () => {
it("should handle CLOSE messages properly", () => { it("should handle CLOSE messages properly", () => {
const cleanup = socketPolicyAuthBuffer(socket) const cleanup = socketPolicyAuthBuffer(socket)
const sendSpy = vi.spyOn(socket, 'send') const sendSpy = vi.spyOn(socket, "send")
socket.emit(SocketEvent.Receive, ["AUTH", "challenge"]) socket.emit(SocketEvent.Receive, ["AUTH", "challenge"])
// Buffer a REQ message // Buffer a REQ message
const req: ClientMessage = ["REQ", "123", { kinds: [1] }] const req: ClientMessage = ["REQ", "123", {kinds: [1]}]
socket.send(req) socket.send(req)
// Send CLOSE for buffered REQ // Send CLOSE for buffered REQ
@@ -109,10 +109,13 @@ describe('policy', () => {
it("should retry events once when auth-required", () => { it("should retry events once when auth-required", () => {
const cleanup = socketPolicyAuthBuffer(socket) const cleanup = socketPolicyAuthBuffer(socket)
const recvQueueRemoveSpy = vi.spyOn(socket._recvQueue, 'remove') const recvQueueRemoveSpy = vi.spyOn(socket._recvQueue, "remove")
// Send an event // Send an event
const event: ClientMessage = ["EVENT", { id: "123", kind: 1, content: "", tags: [], pubkey: "", sig: "" }] const event: ClientMessage = [
"EVENT",
{id: "123", kind: 1, content: "", tags: [], pubkey: "", sig: ""},
]
socket.emit(SocketEvent.Send, event) socket.emit(SocketEvent.Send, event)
// Receive auth-required rejection // Receive auth-required rejection
@@ -134,10 +137,10 @@ describe('policy', () => {
it("should retry REQ once when auth-required", () => { it("should retry REQ once when auth-required", () => {
const cleanup = socketPolicyAuthBuffer(socket) const cleanup = socketPolicyAuthBuffer(socket)
const recvQueueRemoveSpy = vi.spyOn(socket._recvQueue, 'remove') const recvQueueRemoveSpy = vi.spyOn(socket._recvQueue, "remove")
// Send a REQ // Send a REQ
const req: ClientMessage = ["REQ", "123", { kinds: [1] }] const req: ClientMessage = ["REQ", "123", {kinds: [1]}]
socket.emit(SocketEvent.Send, req) socket.emit(SocketEvent.Send, req)
// Receive auth-required rejection // Receive auth-required rejection
@@ -159,10 +162,13 @@ describe('policy', () => {
it("should not retry AUTH_JOIN events", () => { it("should not retry AUTH_JOIN events", () => {
const cleanup = socketPolicyAuthBuffer(socket) const cleanup = socketPolicyAuthBuffer(socket)
const sendSpy = vi.spyOn(socket, 'send') const sendSpy = vi.spyOn(socket, "send")
// Send an AUTH_JOIN event // Send an AUTH_JOIN event
const event: ClientMessage = ["EVENT", { id: "123", kind: AUTH_JOIN, content: "", tags: [], pubkey: "", sig: "" }] const event: ClientMessage = [
"EVENT",
{id: "123", kind: AUTH_JOIN, content: "", tags: [], pubkey: "", sig: ""},
]
socket.emit(SocketEvent.Send, event) socket.emit(SocketEvent.Send, event)
// Receive auth-required rejection // Receive auth-required rejection
@@ -176,10 +182,13 @@ describe('policy', () => {
it("should clear pending messages on successful response", () => { it("should clear pending messages on successful response", () => {
const cleanup = socketPolicyAuthBuffer(socket) const cleanup = socketPolicyAuthBuffer(socket)
const sendSpy = vi.spyOn(socket, 'send') const sendSpy = vi.spyOn(socket, "send")
// Send an event // Send an event
const event: ClientMessage = ["EVENT", { id: "123", kind: 1, content: "", tags: [], pubkey: "", sig: "" }] const event: ClientMessage = [
"EVENT",
{id: "123", kind: 1, content: "", tags: [], pubkey: "", sig: ""},
]
socket.emit(SocketEvent.Send, event) socket.emit(SocketEvent.Send, event)
// Receive successful response // Receive successful response
@@ -198,13 +207,13 @@ describe('policy', () => {
describe("socketPolicyConnectOnSend", () => { describe("socketPolicyConnectOnSend", () => {
it("should open socket on send when closed", () => { it("should open socket on send when closed", () => {
const cleanup = socketPolicyConnectOnSend(socket) const cleanup = socketPolicyConnectOnSend(socket)
const openSpy = vi.spyOn(socket, 'open') const openSpy = vi.spyOn(socket, "open")
// Socket starts closed // Socket starts closed
socket.emit(SocketEvent.Status, SocketStatus.Closed) socket.emit(SocketEvent.Status, SocketStatus.Closed)
// Send a message // Send a message
const event: ClientMessage = ["EVENT", { id: "123", kind: 1 }] const event: ClientMessage = ["EVENT", {id: "123", kind: 1}]
socket.emit(SocketEvent.Sending, event) socket.emit(SocketEvent.Sending, event)
// Should open the socket // Should open the socket
@@ -215,13 +224,13 @@ describe('policy', () => {
it("should not open socket if already open", () => { it("should not open socket if already open", () => {
const cleanup = socketPolicyConnectOnSend(socket) const cleanup = socketPolicyConnectOnSend(socket)
const openSpy = vi.spyOn(socket, 'open') const openSpy = vi.spyOn(socket, "open")
// Socket is open // Socket is open
socket.emit(SocketEvent.Status, SocketStatus.Open) socket.emit(SocketEvent.Status, SocketStatus.Open)
// Send a message // Send a message
const event: ClientMessage = ["EVENT", { id: "123", kind: 1 }] const event: ClientMessage = ["EVENT", {id: "123", kind: 1}]
socket.emit(SocketEvent.Sending, event) socket.emit(SocketEvent.Sending, event)
// Should not try to open the socket // Should not try to open the socket
@@ -232,14 +241,14 @@ describe('policy', () => {
it("should not open socket if there was a recent error", () => { it("should not open socket if there was a recent error", () => {
const cleanup = socketPolicyConnectOnSend(socket) const cleanup = socketPolicyConnectOnSend(socket)
const openSpy = vi.spyOn(socket, 'open') const openSpy = vi.spyOn(socket, "open")
// Socket has an error // Socket has an error
socket.emit(SocketEvent.Status, SocketStatus.Error) socket.emit(SocketEvent.Status, SocketStatus.Error)
socket.emit(SocketEvent.Status, SocketStatus.Closed) socket.emit(SocketEvent.Status, SocketStatus.Closed)
// Send a message // Send a message
const event: ClientMessage = ["EVENT", { id: "123", kind: 1 }] const event: ClientMessage = ["EVENT", {id: "123", kind: 1}]
socket.emit(SocketEvent.Sending, event) socket.emit(SocketEvent.Sending, event)
// Should not try to open the socket due to recent error // Should not try to open the socket due to recent error
@@ -261,7 +270,7 @@ describe('policy', () => {
describe("socketPolicyCloseOnTimeout", () => { describe("socketPolicyCloseOnTimeout", () => {
it("should close socket after 30 seconds of inactivity", async () => { it("should close socket after 30 seconds of inactivity", async () => {
const cleanup = socketPolicyCloseOnTimeout(socket) const cleanup = socketPolicyCloseOnTimeout(socket)
const closeSpy = vi.spyOn(socket, 'close') const closeSpy = vi.spyOn(socket, "close")
// Set socket as open // Set socket as open
socket.emit(SocketEvent.Status, SocketStatus.Open) socket.emit(SocketEvent.Status, SocketStatus.Open)
@@ -277,7 +286,7 @@ describe('policy', () => {
it("should reset timer on send activity", () => { it("should reset timer on send activity", () => {
const cleanup = socketPolicyCloseOnTimeout(socket) const cleanup = socketPolicyCloseOnTimeout(socket)
const closeSpy = vi.spyOn(socket, 'close') const closeSpy = vi.spyOn(socket, "close")
// Set socket as open // Set socket as open
socket.emit(SocketEvent.Status, SocketStatus.Open) socket.emit(SocketEvent.Status, SocketStatus.Open)
@@ -286,7 +295,7 @@ describe('policy', () => {
vi.advanceTimersByTime(20000) vi.advanceTimersByTime(20000)
// Send a message // Send a message
socket.emit(SocketEvent.Send, ["EVENT", { id: "123" }]) socket.emit(SocketEvent.Send, ["EVENT", {id: "123"}])
// Advance time partially again // Advance time partially again
vi.advanceTimersByTime(20000) vi.advanceTimersByTime(20000)
@@ -305,7 +314,7 @@ describe('policy', () => {
it("should reset timer on receive activity", () => { it("should reset timer on receive activity", () => {
const cleanup = socketPolicyCloseOnTimeout(socket) const cleanup = socketPolicyCloseOnTimeout(socket)
const closeSpy = vi.spyOn(socket, 'close') const closeSpy = vi.spyOn(socket, "close")
// Set socket as open // Set socket as open
socket.emit(SocketEvent.Status, SocketStatus.Open) socket.emit(SocketEvent.Status, SocketStatus.Open)
@@ -314,7 +323,7 @@ describe('policy', () => {
vi.advanceTimersByTime(20000) vi.advanceTimersByTime(20000)
// Receive a message // Receive a message
socket.emit(SocketEvent.Receive, ["EVENT", "123", { id: "123" }]) socket.emit(SocketEvent.Receive, ["EVENT", "123", {id: "123"}])
// Advance time partially again // Advance time partially again
vi.advanceTimersByTime(20000) vi.advanceTimersByTime(20000)
@@ -333,7 +342,7 @@ describe('policy', () => {
it("should not close socket if not open", () => { it("should not close socket if not open", () => {
const cleanup = socketPolicyCloseOnTimeout(socket) const cleanup = socketPolicyCloseOnTimeout(socket)
const closeSpy = vi.spyOn(socket, 'close') const closeSpy = vi.spyOn(socket, "close")
// Set socket as closed // Set socket as closed
socket.emit(SocketEvent.Status, SocketStatus.Closed) socket.emit(SocketEvent.Status, SocketStatus.Closed)
@@ -351,10 +360,10 @@ describe('policy', () => {
describe("socketPolicyReopenActive", () => { describe("socketPolicyReopenActive", () => {
it("should reopen socket when closed with pending messages", async () => { it("should reopen socket when closed with pending messages", async () => {
const cleanup = socketPolicyReopenActive(socket) const cleanup = socketPolicyReopenActive(socket)
const sendSpy = vi.spyOn(socket, 'send') const sendSpy = vi.spyOn(socket, "send")
// Send an event that will be pending // Send an event that will be pending
const event: ClientMessage = ["EVENT", { id: "123", kind: 1 }] const event: ClientMessage = ["EVENT", {id: "123", kind: 1}]
socket.emit(SocketEvent.Send, event) socket.emit(SocketEvent.Send, event)
// Socket closes // Socket closes
@@ -371,10 +380,10 @@ describe('policy', () => {
it("should reopen socket when closed with pending requests", async () => { it("should reopen socket when closed with pending requests", async () => {
const cleanup = socketPolicyReopenActive(socket) const cleanup = socketPolicyReopenActive(socket)
const sendSpy = vi.spyOn(socket, 'send') const sendSpy = vi.spyOn(socket, "send")
// Send a request that will be pending // Send a request that will be pending
const req: ClientMessage = ["REQ", "123", { kinds: [1] }] const req: ClientMessage = ["REQ", "123", {kinds: [1]}]
socket.emit(SocketEvent.Send, req) socket.emit(SocketEvent.Send, req)
// Socket closes // Socket closes
@@ -391,10 +400,10 @@ describe('policy', () => {
it("should not reopen socket immediately after previous open", async () => { it("should not reopen socket immediately after previous open", async () => {
const cleanup = socketPolicyReopenActive(socket) const cleanup = socketPolicyReopenActive(socket)
const sendSpy = vi.spyOn(socket, 'send') const sendSpy = vi.spyOn(socket, "send")
// Send an event that will be pending // Send an event that will be pending
const event: ClientMessage = ["EVENT", { id: "123", kind: 1 }] const event: ClientMessage = ["EVENT", {id: "123", kind: 1}]
socket.emit(SocketEvent.Send, event) socket.emit(SocketEvent.Send, event)
// Socket opens then closes quickly // Socket opens then closes quickly
@@ -418,10 +427,10 @@ describe('policy', () => {
it("should remove pending messages when they complete", () => { it("should remove pending messages when they complete", () => {
const cleanup = socketPolicyReopenActive(socket) const cleanup = socketPolicyReopenActive(socket)
const sendSpy = vi.spyOn(socket, 'send') const sendSpy = vi.spyOn(socket, "send")
// Send an event that will be pending // Send an event that will be pending
const event: ClientMessage = ["EVENT", { id: "123", kind: 1 }] const event: ClientMessage = ["EVENT", {id: "123", kind: 1}]
socket.emit(SocketEvent.Send, event) socket.emit(SocketEvent.Send, event)
// Event completes successfully // Event completes successfully
@@ -441,10 +450,10 @@ describe('policy', () => {
it("should remove pending messages when closed", () => { it("should remove pending messages when closed", () => {
const cleanup = socketPolicyReopenActive(socket) const cleanup = socketPolicyReopenActive(socket)
const sendSpy = vi.spyOn(socket, 'send') const sendSpy = vi.spyOn(socket, "send")
// Send a request that will be pending // Send a request that will be pending
const req: ClientMessage = ["REQ", "123", { kinds: [1] }] const req: ClientMessage = ["REQ", "123", {kinds: [1]}]
socket.emit(SocketEvent.Send, req) socket.emit(SocketEvent.Send, req)
// Send close for the request // Send close for the request
+7 -8
View File
@@ -1,9 +1,8 @@
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest" import {describe, expect, it, vi, beforeEach, afterEach} from "vitest"
import { Socket } from "../src/socket" import {Socket} from "../src/socket"
import { Pool, makeSocket } from "../src/pool" import {Pool} from "../src/pool"
import { normalizeRelayUrl } from "@welshman/util"
vi.mock('isomorphic-ws', () => { vi.mock("isomorphic-ws", () => {
const WebSocket = vi.fn(function (this: any) { const WebSocket = vi.fn(function (this: any) {
setTimeout(() => this.onopen()) setTimeout(() => this.onopen())
}) })
@@ -14,7 +13,7 @@ vi.mock('isomorphic-ws', () => {
this.onclose() this.onclose()
}) })
return { default: WebSocket } return {default: WebSocket}
}) })
describe("Pool", () => { describe("Pool", () => {
@@ -95,7 +94,7 @@ describe("Pool", () => {
describe("remove", () => { describe("remove", () => {
it("should remove and cleanup existing socket", () => { it("should remove and cleanup existing socket", () => {
const mockSocket = { url: "wss://test.relay", cleanup: vi.fn() } const mockSocket = {url: "wss://test.relay", cleanup: vi.fn()}
pool._data.set(mockSocket.url, mockSocket as unknown as Socket) pool._data.set(mockSocket.url, mockSocket as unknown as Socket)
pool.remove(mockSocket.url) pool.remove(mockSocket.url)
@@ -113,7 +112,7 @@ describe("Pool", () => {
describe("clear", () => { describe("clear", () => {
it("should remove all sockets", () => { it("should remove all sockets", () => {
const urls = ["wss://test1.relay", "wss://test2.relay"] const urls = ["wss://test1.relay", "wss://test2.relay"]
const mockSockets = urls.map(url => ({ url, cleanup: vi.fn() })) const mockSockets = urls.map(url => ({url, cleanup: vi.fn()}))
for (const mockSocket of mockSockets) { for (const mockSocket of mockSockets) {
pool._data.set(mockSocket.url, mockSocket as unknown as Socket) pool._data.set(mockSocket.url, mockSocket as unknown as Socket)
+28 -26
View File
@@ -1,10 +1,9 @@
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest" import {describe, expect, it, vi, beforeEach, afterEach} from "vitest"
import { EventEmitter } from "events" import {SinglePublish, MultiPublish, PublishEvent} from "../src/publish"
import { SinglePublish, MultiPublish, PublishEvent, PublishStatus, } from "../src/publish" import {MockAdapter} from "../src/adapter"
import { AbstractAdapter, AdapterEvent, MockAdapter } from "../src/adapter" import {ClientMessageType} from "../src/message"
import { ClientMessageType, RelayMessage } from "../src/message" import {makeEvent} from "@welshman/util"
import { SignedEvent, makeEvent } from "@welshman/util" import {Nip01Signer} from "@welshman/signer"
import { Nip01Signer } from '@welshman/signer'
describe("SinglePublish", () => { describe("SinglePublish", () => {
beforeEach(() => { beforeEach(() => {
@@ -17,12 +16,12 @@ describe("SinglePublish", () => {
it("success works", async () => { it("success works", async () => {
const sendSpy = vi.fn() const sendSpy = vi.fn()
const adapter = new MockAdapter('1', sendSpy) const adapter = new MockAdapter("1", sendSpy)
const signer = Nip01Signer.ephemeral() const signer = Nip01Signer.ephemeral()
const event = await signer.sign(makeEvent(1)) const event = await signer.sign(makeEvent(1))
const pub = new SinglePublish({ const pub = new SinglePublish({
relay: '1', relay: "1",
context: {getAdapter: () => adapter}, context: {getAdapter: () => adapter},
event, event,
}) })
@@ -50,12 +49,12 @@ describe("SinglePublish", () => {
it("failure works", async () => { it("failure works", async () => {
const sendSpy = vi.fn() const sendSpy = vi.fn()
const adapter = new MockAdapter('1', sendSpy) const adapter = new MockAdapter("1", sendSpy)
const signer = Nip01Signer.ephemeral() const signer = Nip01Signer.ephemeral()
const event = await signer.sign(makeEvent(1)) const event = await signer.sign(makeEvent(1))
const pub = new SinglePublish({ const pub = new SinglePublish({
relay: '1', relay: "1",
context: {getAdapter: () => adapter}, context: {getAdapter: () => adapter},
event, event,
}) })
@@ -83,12 +82,12 @@ describe("SinglePublish", () => {
it("timeout works", async () => { it("timeout works", async () => {
const sendSpy = vi.fn() const sendSpy = vi.fn()
const adapter = new MockAdapter('1', sendSpy) const adapter = new MockAdapter("1", sendSpy)
const signer = Nip01Signer.ephemeral() const signer = Nip01Signer.ephemeral()
const event = await signer.sign(makeEvent(1)) const event = await signer.sign(makeEvent(1))
const pub = new SinglePublish({ const pub = new SinglePublish({
relay: '1', relay: "1",
context: {getAdapter: () => adapter}, context: {getAdapter: () => adapter},
event, event,
}) })
@@ -117,12 +116,12 @@ describe("SinglePublish", () => {
it("abort works", async () => { it("abort works", async () => {
const sendSpy = vi.fn() const sendSpy = vi.fn()
const adapter = new MockAdapter('1', sendSpy) const adapter = new MockAdapter("1", sendSpy)
const signer = Nip01Signer.ephemeral() const signer = Nip01Signer.ephemeral()
const event = await signer.sign(makeEvent(1)) const event = await signer.sign(makeEvent(1))
const pub = new SinglePublish({ const pub = new SinglePublish({
relay: '1', relay: "1",
context: {getAdapter: () => adapter}, context: {getAdapter: () => adapter},
event, event,
}) })
@@ -163,27 +162,31 @@ describe("MultiPublish", () => {
it("should all basically work", async () => { it("should all basically work", async () => {
const send1Spy = vi.fn() const send1Spy = vi.fn()
const adapter1 = new MockAdapter('1', send1Spy) const adapter1 = new MockAdapter("1", send1Spy)
const send2Spy = vi.fn() const send2Spy = vi.fn()
const adapter2 = new MockAdapter('2', send2Spy) const adapter2 = new MockAdapter("2", send2Spy)
const send3Spy = vi.fn() const send3Spy = vi.fn()
const adapter3 = new MockAdapter('3', send3Spy) const adapter3 = new MockAdapter("3", send3Spy)
const signer = Nip01Signer.ephemeral() const signer = Nip01Signer.ephemeral()
const event = await signer.sign(makeEvent(1)) const event = await signer.sign(makeEvent(1))
const pub = new MultiPublish({ const pub = new MultiPublish({
event, event,
relays: ['1', '2', '3'], relays: ["1", "2", "3"],
context: { context: {
getAdapter: (url: string) => { getAdapter: (url: string) => {
switch(url) { switch (url) {
case '1': return adapter1 case "1":
case '2': return adapter2 return adapter1
case '3': return adapter3 case "2":
default: throw new Error(`Unknown relay: ${url}`) return adapter2
case "3":
return adapter3
default:
throw new Error(`Unknown relay: ${url}`)
} }
}, },
} },
}) })
const successSpy = vi.fn() const successSpy = vi.fn()
@@ -199,7 +202,6 @@ describe("MultiPublish", () => {
adapter1.receive(["OK", event.id, true, "hi"]) adapter1.receive(["OK", event.id, true, "hi"])
adapter2.receive(["OK", event.id, false, "hi"]) adapter2.receive(["OK", event.id, false, "hi"])
await vi.runAllTimers() await vi.runAllTimers()
expect(successSpy).toHaveBeenCalledWith(event.id, "hi", "1") expect(successSpy).toHaveBeenCalledWith(event.id, "hi", "1")
+16 -17
View File
@@ -1,9 +1,9 @@
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest" import {describe, expect, it, vi, beforeEach, afterEach} from "vitest"
import { Nip01Signer } from '@welshman/signer' import {Nip01Signer} from "@welshman/signer"
import { makeEvent } from '@welshman/util' import {makeEvent} from "@welshman/util"
import { ClientMessageType } from "../src/message" import {ClientMessageType} from "../src/message"
import { MockAdapter } from "../src/adapter" import {MockAdapter} from "../src/adapter"
import { SingleRequest, MultiRequest, RequestEvent } from "../src/request" import {SingleRequest, MultiRequest, RequestEvent} from "../src/request"
describe("SingleRequest", () => { describe("SingleRequest", () => {
beforeEach(() => { beforeEach(() => {
@@ -16,9 +16,9 @@ describe("SingleRequest", () => {
it("everything basically works", async () => { it("everything basically works", async () => {
const sendSpy = vi.fn() const sendSpy = vi.fn()
const adapter = new MockAdapter('1', sendSpy) const adapter = new MockAdapter("1", sendSpy)
const req = new SingleRequest({ const req = new SingleRequest({
relay: 'whatever', relay: "whatever",
filters: [{kinds: [1]}], filters: [{kinds: [1]}],
context: {getAdapter: () => adapter}, context: {getAdapter: () => adapter},
}) })
@@ -82,14 +82,14 @@ describe("MultiRequest", () => {
it("everything basically works", async () => { it("everything basically works", async () => {
const send1Spy = vi.fn() const send1Spy = vi.fn()
const adapter1 = new MockAdapter('1', send1Spy) const adapter1 = new MockAdapter("1", send1Spy)
const send2Spy = vi.fn() const send2Spy = vi.fn()
const adapter2 = new MockAdapter('2', send2Spy) const adapter2 = new MockAdapter("2", send2Spy)
const req = new MultiRequest({ const req = new MultiRequest({
relays: ['1', '2'], relays: ["1", "2"],
filters: [{kinds: [1]}], filters: [{kinds: [1]}],
context: { context: {
getAdapter: (url: string) => url === '1' ? adapter1 : adapter2 getAdapter: (url: string) => (url === "1" ? adapter1 : adapter2),
}, },
}) })
@@ -129,10 +129,10 @@ describe("MultiRequest", () => {
await vi.runAllTimersAsync() await vi.runAllTimersAsync()
expect(duplicateSpy).toHaveBeenCalledWith(event1, '2') expect(duplicateSpy).toHaveBeenCalledWith(event1, "2")
expect(filteredSpy).toHaveBeenCalledWith(event2, '1') expect(filteredSpy).toHaveBeenCalledWith(event2, "1")
expect(invalidSpy).toHaveBeenCalledWith(event3, '1') expect(invalidSpy).toHaveBeenCalledWith(event3, "1")
expect(eventSpy).toHaveBeenCalledWith(event1, '1') expect(eventSpy).toHaveBeenCalledWith(event1, "1")
expect(eoseSpy).toHaveBeenCalledTimes(0) expect(eoseSpy).toHaveBeenCalledTimes(0)
adapter1.receive(["EOSE", id1]) adapter1.receive(["EOSE", id1])
@@ -145,4 +145,3 @@ describe("MultiRequest", () => {
expect(closeSpy).toHaveBeenCalledTimes(1) expect(closeSpy).toHaveBeenCalledTimes(1)
}) })
}) })
+12 -11
View File
@@ -1,10 +1,9 @@
import { sleep } from "@welshman/lib" import WebSocket from "isomorphic-ws"
import WebSocket from 'isomorphic-ws' import {afterEach, beforeEach, describe, expect, it, vi} from "vitest"
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest" import {Socket, SocketStatus, SocketEvent} from "../src/socket"
import { Socket, SocketStatus, SocketEvent } from "../src/socket" import {ClientMessage, RelayMessage} from "../src/message"
import { ClientMessage, RelayMessage } from "../src/message"
vi.mock('isomorphic-ws', () => { vi.mock("isomorphic-ws", () => {
const WebSocket = vi.fn(function (this: any) { const WebSocket = vi.fn(function (this: any) {
setTimeout(() => this.onopen()) setTimeout(() => this.onopen())
}) })
@@ -15,7 +14,7 @@ vi.mock('isomorphic-ws', () => {
this.onclose() this.onclose()
}) })
return { default: WebSocket } return {default: WebSocket}
}) })
describe("Socket", () => { describe("Socket", () => {
@@ -77,9 +76,11 @@ describe("Socket", () => {
socket.open() socket.open()
const closeSpy = vi.spyOn(socket._ws, "close")
socket.close() socket.close()
expect(socket._ws!.close).toHaveBeenCalled() expect(closeSpy).toHaveBeenCalled()
expect(statusSpy).toHaveBeenCalledWith(SocketStatus.Closed, "wss://test.relay") expect(statusSpy).toHaveBeenCalledWith(SocketStatus.Closed, "wss://test.relay")
}) })
}) })
@@ -89,7 +90,7 @@ describe("Socket", () => {
const enqueueSpy = vi.fn() const enqueueSpy = vi.fn()
socket.on(SocketEvent.Sending, enqueueSpy) socket.on(SocketEvent.Sending, enqueueSpy)
const message: ClientMessage = ["EVENT", { id: "123", kind: 1 }] const message: ClientMessage = ["EVENT", {id: "123", kind: 1}]
socket.send(message) socket.send(message)
expect(enqueueSpy).toHaveBeenCalledWith(message, "wss://test.relay") expect(enqueueSpy).toHaveBeenCalledWith(message, "wss://test.relay")
@@ -102,7 +103,7 @@ describe("Socket", () => {
socket.open() socket.open()
socket._ws?.onopen?.(undefined as unknown as any) socket._ws?.onopen?.(undefined as unknown as any)
const message: ClientMessage = ["EVENT", { id: "123", kind: 1 }] const message: ClientMessage = ["EVENT", {id: "123", kind: 1}]
socket.send(message) socket.send(message)
await vi.runAllTimers() await vi.runAllTimers()
@@ -118,7 +119,7 @@ describe("Socket", () => {
socket.on(SocketEvent.Receive, receiveSpy) socket.on(SocketEvent.Receive, receiveSpy)
socket.open() socket.open()
const message: RelayMessage = ["EVENT", "123", { id: "123", kind: 1 }] const message: RelayMessage = ["EVENT", "123", {id: "123", kind: 1}]
socket._ws?.onmessage?.({data: JSON.stringify(message)} as unknown as any) socket._ws?.onmessage?.({data: JSON.stringify(message)} as unknown as any)
await vi.runAllTimers() await vi.runAllTimers()
File diff suppressed because it is too large Load Diff
+3 -4
View File
@@ -1,4 +1,4 @@
import {on, nthEq, always, call, sleep, spec, ago, now} from "@welshman/lib" import {on, nthEq, always, call, sleep, ago, now} from "@welshman/lib"
import {AUTH_JOIN, StampedEvent, SignedEvent} from "@welshman/util" import {AUTH_JOIN, StampedEvent, SignedEvent} from "@welshman/util"
import { import {
ClientMessage, ClientMessage,
@@ -7,7 +7,6 @@ import {
isClientEvent, isClientEvent,
isClientReq, isClientReq,
isClientNegClose, isClientNegClose,
ClientMessageType,
RelayMessage, RelayMessage,
isRelayOk, isRelayOk,
isRelayEose, isRelayEose,
@@ -50,7 +49,7 @@ export const socketPolicyAuthBuffer = (socket: Socket) => {
}), }),
on(socket, SocketEvent.Receiving, (message: RelayMessage) => { on(socket, SocketEvent.Receiving, (message: RelayMessage) => {
// If the client is closing a request during auth, don't tell the caller, we'll retry it // If the client is closing a request during auth, don't tell the caller, we'll retry it
if (isRelayClosed(message) && message[2]?.startsWith('auth-required:')) { if (isRelayClosed(message) && message[2]?.startsWith("auth-required:")) {
socket._recvQueue.remove(message) socket._recvQueue.remove(message)
} }
@@ -60,7 +59,7 @@ export const socketPolicyAuthBuffer = (socket: Socket) => {
} }
// If the client is rejecting an event during auth, don't tell the caller, we'll retry it // If the client is rejecting an event during auth, don't tell the caller, we'll retry it
if (isRelayOk(message) && !message[2] && message[3]?.startsWith('auth-required:')) { if (isRelayOk(message) && !message[2] && message[3]?.startsWith("auth-required:")) {
socket._recvQueue.remove(message) socket._recvQueue.remove(message)
} }
}), }),
+1 -1
View File
@@ -289,7 +289,7 @@ export const makeLoader = (options: LoaderOptions) =>
tracker, tracker,
relays: [relay], relays: [relay],
autoClose: true, autoClose: true,
...options ...options,
}) })
let count = 0 let count = 0
-1
View File
@@ -19,7 +19,6 @@ describe("LocalRelay", () => {
const id = "ff".repeat(32) const id = "ff".repeat(32)
const sig = "00".repeat(64) const sig = "00".repeat(64)
const currentTime = now() const currentTime = now()
const onionUrl = "abcdefghijklmnopqrstuvwxyz234567abcdefghijklmnopqrstuvwx.onion"
const createEvent = (overrides = {}): TrustedEvent => ({ const createEvent = (overrides = {}): TrustedEvent => ({
id: id, id: id,
-23
View File
@@ -3,7 +3,6 @@ import {Repository} from "@welshman/relay"
import {get} from "svelte/store" import {get} from "svelte/store"
import {afterEach, beforeEach, describe, expect, it, vi} from "vitest" import {afterEach, beforeEach, describe, expect, it, vi} from "vitest"
import { import {
adapter,
custom, custom,
deriveEvents, deriveEvents,
deriveEventsMapped, deriveEventsMapped,
@@ -114,28 +113,6 @@ describe("Store utilities", () => {
}) })
}) })
describe("adapter", () => {
it("should adapt between different types", () => {
const source = synced<number>("test", 0)
const adapted = adapter({
store: source,
forward: n => n.toString(),
backward: s => parseInt(s, 10),
})
const mockFn = vi.fn()
adapted.subscribe(mockFn)
adapted.set("42")
expect(get(source)).toBe(42)
expect(mockFn).toHaveBeenLastCalledWith("42")
adapted.update(s => (parseInt(s, 10) + 1).toString())
expect(get(source)).toBe(43)
expect(mockFn).toHaveBeenLastCalledWith("43")
})
})
describe("Event-related stores", () => { describe("Event-related stores", () => {
const mockRepository = { const mockRepository = {
query: vi.fn(), query: vi.fn(),
-16
View File
@@ -121,22 +121,6 @@ export const custom = <T>(
} }
} }
// Simple adapter
export const adapter = <Source, Target>({
store,
forward,
backward,
}: {
store: Writable<Source>
forward: (x: Source) => Target
backward: (x: Target) => Source
}) => ({
...derived(store, forward),
set: (x: Target) => store.set(backward(x)),
update: (f: (x: Target) => Target) => store.update((x: Source) => backward(f(forward(x)))),
})
// Event related stores // Event related stores
export type DeriveEventsMappedOptions<T> = { export type DeriveEventsMappedOptions<T> = {
+1 -1
View File
@@ -184,7 +184,7 @@ describe("Events", () => {
describe("signature validation", () => { describe("signature validation", () => {
it("should validate signature using verifiedSymbol", () => { it("should validate signature using verifiedSymbol", () => {
let event = createSignedEvent() as Events.SignedEvent const event = createSignedEvent() as Events.SignedEvent
event[verifiedSymbol] = true event[verifiedSymbol] = true
expect(Events.verifyEvent(event)).toBe(true) expect(Events.verifyEvent(event)).toBe(true)
+1 -3
View File
@@ -211,9 +211,7 @@ describe("Filters", () => {
it("should calculate filter generality", () => { it("should calculate filter generality", () => {
expect(getFilterGenerality({ids: [id]})).toBe(0) expect(getFilterGenerality({ids: [id]})).toBe(0)
expect(getFilterGenerality({authors: [pubkey], "#p": [pubkey]})).toBe(0.2) expect(getFilterGenerality({authors: [pubkey], "#p": [pubkey]})).toBe(0.2)
expect(getFilterGenerality({authors: [pubkey, pubkey, pubkey], kinds: [1]})).toBe( expect(getFilterGenerality({authors: [pubkey, pubkey, pubkey], kinds: [1]})).toBe(0.01)
0.01,
)
expect(getFilterGenerality({kinds: [1]})).toBe(1) expect(getFilterGenerality({kinds: [1]})).toBe(1)
}) })
+2 -1
View File
@@ -72,7 +72,8 @@ export const verifyEvent = (() => {
}) })
} }
return (event: TrustedEvent) => event.sig && (event[verifiedSymbol] || verify(event as SignedEvent)) return (event: TrustedEvent) =>
event.sig && (event[verifiedSymbol] || verify(event as SignedEvent))
})() })()
export const isEventTemplate = (e: EventTemplate): e is EventTemplate => export const isEventTemplate = (e: EventTemplate): e is EventTemplate =>
-1157
View File
File diff suppressed because it is too large Load Diff