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
+6 -10
View File
@@ -1,10 +1,7 @@
import {now} from "@welshman/lib"
import {PublishStatus, MockAdapter} from "@welshman/net"
import {NOTE, makeEvent} from "@welshman/util"
import {Nip01Signer} from "@welshman/signer"
import {PublishStatus} from "@welshman/net"
import {NOTE, makeEvent} from "@welshman/util"
import {LOCAL_RELAY_URL} from "@welshman/relay"
import {getPubkey, makeSecret} from "@welshman/signer"
import {EventEmitter} from "events"
import {afterEach, beforeEach, describe, expect, it, vi} from "vitest"
import {repository, tracker} from "../src/core"
import {addSession, dropSession} from "../src/session"
@@ -31,7 +28,7 @@ const mockRequest = {
describe("thunk", () => {
beforeEach(() => {
vi.useFakeTimers()
addSession({method: 'nip01', secret, pubkey})
addSession({method: "nip01", secret, pubkey})
})
afterEach(async () => {
@@ -70,7 +67,7 @@ describe("thunk", () => {
describe("publishThunk", () => {
it("should create and publish a thunk", async () => {
const publishSpy = vi.spyOn(repository, 'publish')
const publishSpy = vi.spyOn(repository, "publish")
const result = publishThunk(mockRequest)
expect(publishSpy).toHaveBeenCalled()
@@ -79,7 +76,7 @@ describe("thunk", () => {
})
it("should handle abort", () => {
const removeEventSpy = vi.spyOn(repository, 'removeEvent')
const removeEventSpy = vi.spyOn(repository, "removeEvent")
const thunk = publishThunk(mockRequest)
thunk.controller.abort()
@@ -109,8 +106,7 @@ describe("thunk", () => {
})
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)
let status: Record<string, any> = {}
+23 -15
View File
@@ -1,17 +1,25 @@
import {get, derived} from 'svelte/store'
import {batch, fromPairs} from '@welshman/lib'
import {PROFILE, FOLLOWS, MUTES, RELAYS, INBOX_RELAYS, getPubkeyTagValues, getListTags} from '@welshman/util'
import {throttled, withGetter} from '@welshman/store'
import {RepositoryUpdate} from '@welshman/relay'
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'
import {derived} from "svelte/store"
import {batch, fromPairs} from "@welshman/lib"
import {
PROFILE,
FOLLOWS,
MUTES,
RELAYS,
INBOX_RELAYS,
getPubkeyTagValues,
getListTags,
} from "@welshman/util"
import {throttled, withGetter} from "@welshman/store"
import {RepositoryUpdate} from "@welshman/relay"
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 = {
relays: {
@@ -70,7 +78,7 @@ export const defaultStorageAdapters = {
init: async () => repository.load(await getAll("events")),
sync: () => {
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) => {
+3 -1
View File
@@ -33,7 +33,9 @@ export const requestDVM = async ({kind, onEvent, ...request}: DVMOpts) => {
const tags = request.tags || []
const $signer = signer.get() || new Nip01Signer(makeSecret())
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"))) {
tags.push(["expiration", String(now() + 60)])
-2
View File
@@ -1,9 +1,7 @@
import {FOLLOWS, asDecryptedEvent, readList} from "@welshman/util"
import {TrustedEvent, PublishedList} from "@welshman/util"
import {MultiRequestOptions, load} from "@welshman/net"
import {deriveEventsMapped} from "@welshman/store"
import {repository} from "./core.js"
import {Router} from "./router.js"
import {collection} from "./collection.js"
import {loadWithAsapMetaRelayUrls} from "./relaySelections.js"
-1
View File
@@ -1,5 +1,4 @@
import {writable, derived} from "svelte/store"
import {MultiRequestOptions} from "@welshman/net"
import {tryCatch, fetchJson, uniq, batcher, postJson, last} from "@welshman/lib"
import {collection} from "./collection.js"
import {deriveProfile} from "./profiles.js"
-2
View File
@@ -1,9 +1,7 @@
import {MUTES, asDecryptedEvent, readList} from "@welshman/util"
import {TrustedEvent, PublishedList} from "@welshman/util"
import {load, MultiRequestOptions} from "@welshman/net"
import {deriveEventsMapped} from "@welshman/store"
import {repository} from "./core.js"
import {Router} from "./router.js"
import {collection} from "./collection.js"
import {ensurePlaintext} from "./plaintext.js"
import {loadWithAsapMetaRelayUrls} from "./relaySelections.js"
-2
View File
@@ -1,9 +1,7 @@
import {PINS, asDecryptedEvent, readList} from "@welshman/util"
import {TrustedEvent, PublishedList} from "@welshman/util"
import {load, MultiRequestOptions} from "@welshman/net"
import {deriveEventsMapped} from "@welshman/store"
import {repository} from "./core.js"
import {Router} from "./router.js"
import {collection} from "./collection.js"
import {loadWithAsapMetaRelayUrls} from "./relaySelections.js"
+1 -3
View File
@@ -1,10 +1,8 @@
import {derived, readable} from "svelte/store"
import {readProfile, displayProfile, displayPubkey, PROFILE} from "@welshman/util"
import {load, MultiRequestOptions} from "@welshman/net"
import {PublishedProfile} from "@welshman/util"
import {deriveEventsMapped, withGetter} from "@welshman/store"
import {repository} from "./core.js"
import {Router} from "./router.js"
import {collection} from "./collection.js"
import {loadWithAsapMetaRelayUrls} from "./relaySelections.js"
@@ -25,7 +23,7 @@ export const {
store: profiles,
getKey: profile => profile.event.pubkey,
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) =>
+11 -7
View File
@@ -10,10 +10,10 @@ import {
getRelayTagValues,
} 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 {repository} from "./core.js"
import {Router, addNoFallbacks} from "./router.js"
import {Router} from "./router.js"
import {collection} from "./collection.js"
export const getRelayUrls = (list?: List): string[] =>
@@ -51,13 +51,15 @@ export const {
const router = Router.get()
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]}],
})
},
})
export const loadWithAsapMetaRelayUrls = <T>(pubkey: string, relays: string[], filters: Filter[]) => {
export const loadWithAsapMetaRelayUrls = (pubkey: string, relays: string[], filters: Filter[]) => {
const router = Router.get()
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()})
.then(onLoad)
load({
filters,
relays: router.merge([router.Index(), router.FromRelays(relays)]).getUrls(),
}).then(onLoad)
loadRelaySelections(pubkey, relays)
.then(() => load({filters, relays: router.FromPubkey(pubkey).getUrls()}))
@@ -93,5 +97,5 @@ export const {
store: inboxRelaySelections,
getKey: inboxRelaySelections => inboxRelaySelections.event.pubkey,
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,
HOUR,
DAY,
WEEK,
} from "@welshman/lib"
import {
getFilterId,
@@ -242,7 +241,8 @@ export class Router {
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)))
@@ -493,7 +493,9 @@ export const getFilterSelections = (
const result = []
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()})
}
+3 -6
View File
@@ -1,12 +1,9 @@
import {openDB, deleteDB} from "idb"
import {IDBPDatabase} from "idb"
import {writable} from "svelte/store"
import {Unsubscriber, Writable} from "svelte/store"
import {indexBy, call, equals, throttle, fromPairs} from "@welshman/lib"
import {TrustedEvent} from "@welshman/util"
import {Repository} from "@welshman/relay"
import {Tracker} from "@welshman/net"
import {withGetter, adapter, throttled, custom} from "@welshman/store"
import {Unsubscriber} from "svelte/store"
import {call} from "@welshman/lib"
import {withGetter} from "@welshman/store"
export type StorageAdapterOptions = {
throttle?: number
+16 -4
View File
@@ -1,5 +1,15 @@
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 {
TrustedEvent,
@@ -225,9 +235,11 @@ export const thunkQueue = new TaskQueue<Thunk>({
// Update status to pending
thunk.status.set(
fromPairs(
thunk.request.relays
.map(url => [url, {status: PublishStatus.Pending, message: "Sending your message..."}])
)
thunk.request.relays.map(url => [
url,
{status: PublishStatus.Pending, message: "Sending your message..."},
]),
),
)
// Send it off
-1
View File
@@ -1,6 +1,5 @@
import {writable, derived} from "svelte/store"
import {Zapper} from "@welshman/util"
import {MultiRequestOptions} from "@welshman/net"
import {
identity,
fetchJson,
+1 -3
View File
@@ -8,9 +8,7 @@ describe("Content Parsing", () => {
describe("Basic Parsing", () => {
it("should parse plain text", () => {
const result = parse({content: "Hello world"})
expect(result).toEqual([
{type: ParsedType.Text, value: "Hello world", raw: "Hello world"},
])
expect(result).toEqual([{type: ParsedType.Text, value: "Hello world", raw: "Hello world"}])
})
it("should parse newlines", () => {
+2 -2
View File
@@ -1,5 +1,5 @@
export * from "./nodeviews/index.js"
export * from "./extensions/index.js"
export * from "./plugins/index.js"
export {Editor, NodeViewProps} from '@tiptap/core'
export {UploadTask} from 'nostr-editor'
export {Editor, NodeViewProps} from "@tiptap/core"
export {UploadTask} from "nostr-editor"
+3 -2
View File
@@ -1,6 +1,6 @@
import {bech32, utf8} from "@scure/base"
type Obj<T = any> = Record<string, T>;
type Obj<T = any> = Record<string, T>
// ----------------------------------------------------------------------------
// 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)
/** 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 */
export const min = (xs: (number | undefined)[]) => {
+261 -253
View File
@@ -1,12 +1,13 @@
// Copied from https://github.com/sindresorhus/normalize-url
/* eslint-disable */
export type Options = {
/**
/**
@default 'http'
*/
readonly defaultProtocol?: 'https' | 'http';
readonly defaultProtocol?: "https" | "http"
/**
/**
Prepends `defaultProtocol` to the URL if it's protocol-relative.
@default true
@@ -20,9 +21,9 @@ export type Options = {
//=> '//sindresorhus.com'
```
*/
readonly normalizeProtocol?: boolean;
readonly normalizeProtocol?: boolean
/**
/**
Normalizes HTTPS URLs to HTTP.
@default false
@@ -36,9 +37,9 @@ export type Options = {
//=> 'http://sindresorhus.com'
```
*/
readonly forceHttp?: boolean;
readonly forceHttp?: boolean
/**
/**
Normalizes HTTP URLs to HTTPS.
This option cannot be used with the `forceHttp` option at the same time.
@@ -54,9 +55,9 @@ export type Options = {
//=> 'https://sindresorhus.com'
```
*/
readonly forceHttps?: boolean;
readonly forceHttps?: boolean
/**
/**
Strip the [authentication](https://en.wikipedia.org/wiki/Basic_access_authentication) part of a URL.
@default true
@@ -70,9 +71,9 @@ export type Options = {
//=> 'https://user:password@sindresorhus.com'
```
*/
readonly stripAuthentication?: boolean;
readonly stripAuthentication?: boolean
/**
/**
Removes hash from the URL.
@default false
@@ -86,9 +87,9 @@ export type Options = {
//=> 'http://sindresorhus.com/about.html'
```
*/
readonly stripHash?: boolean;
readonly stripHash?: boolean
/**
/**
Remove the protocol from the URL: `http://sindresorhus.com` → `sindresorhus.com`.
It will only remove `https://` and `http://` protocols.
@@ -104,9 +105,9 @@ export type Options = {
//=> 'sindresorhus.com'
```
*/
readonly stripProtocol?: boolean;
readonly stripProtocol?: boolean
/**
/**
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.
@@ -128,9 +129,9 @@ export type Options = {
//=> 'http://sindresorhus.com/about.html#section:~:text=hello'
```
*/
readonly stripTextFragment?: boolean;
readonly stripTextFragment?: boolean
/**
/**
Removes `www.` from the URL.
@default true
@@ -144,9 +145,9 @@ export type Options = {
//=> 'http://www.sindresorhus.com'
```
*/
readonly stripWWW?: boolean;
readonly stripWWW?: boolean
/**
/**
Removes query parameters that matches any of the provided strings or regexes.
@default [/^utm_\w+/i]
@@ -177,9 +178,9 @@ export type Options = {
//=> '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.
__Note__: It overrides the `removeQueryParameters` option.
@@ -194,9 +195,9 @@ export type Options = {
//=> 'https://sindresorhus.com/?ref=unicorn'
```
*/
readonly keepQueryParameters?: ReadonlyArray<RegExp | string>;
readonly keepQueryParameters?: ReadonlyArray<RegExp | string>
/**
/**
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`.
@@ -215,9 +216,9 @@ export type Options = {
//=> 'http://sindresorhus.com'
```
*/
readonly removeTrailingSlash?: boolean;
readonly removeTrailingSlash?: boolean
/**
/**
Remove a sole `/` pathname in the output. This option is independent of `removeTrailingSlash`.
@default true
@@ -231,9 +232,9 @@ export type Options = {
//=> '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.
When `true`, the regex `/^index\.[a-z]+$/` is used.
@@ -247,9 +248,9 @@ export type Options = {
//=> 'http://sindresorhus.com/foo'
```
*/
readonly removeDirectoryIndex?: boolean | ReadonlyArray<RegExp | string>;
readonly removeDirectoryIndex?: boolean | ReadonlyArray<RegExp | string>
/**
/**
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.
@@ -264,9 +265,9 @@ export type Options = {
//=> 'http://sindresorhus.com'
```
*/
readonly removeExplicitPort?: boolean;
readonly removeExplicitPort?: boolean
/**
/**
Sorts the query parameters alphabetically by key.
@default true
@@ -279,79 +280,74 @@ export type Options = {
//=> '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
const DATA_URL_DEFAULT_MIME_TYPE = 'text/plain'
const DATA_URL_DEFAULT_CHARSET = 'us-ascii'
const DATA_URL_DEFAULT_MIME_TYPE = "text/plain"
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([
'https:',
'http:',
'file:',
])
const supportedProtocols = new Set(["https:", "http:", "file:"])
const hasCustomProtocol = (urlString: string) => {
try {
const {protocol} = new URL(urlString)
return protocol.endsWith(':') && !supportedProtocols.has(protocol)
} catch {
return false
}
try {
const {protocol} = new URL(urlString)
return protocol.endsWith(":") && !supportedProtocols.has(protocol)
} catch {
return false
}
}
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) {
throw new Error(`Invalid URL: ${urlString}`)
}
if (!match) {
throw new Error(`Invalid URL: ${urlString}`)
}
let {type, data, hash} = match.groups as any
const mediaType = type.split(';')
hash = stripHash ? '' : hash
let {type, data, hash} = match.groups as any
const mediaType = type.split(";")
hash = stripHash ? "" : hash
let isBase64 = false
if (mediaType[mediaType.length - 1] === 'base64') {
mediaType.pop()
isBase64 = true
}
let isBase64 = false
if (mediaType[mediaType.length - 1] === "base64") {
mediaType.pop()
isBase64 = true
}
// Lowercase MIME type
const mimeType = mediaType.shift()?.toLowerCase() ?? ''
const attributes = mediaType
.map((attribute: string) => {
let [key, value = ''] = attribute.split('=').map((s: string) => s.trim())
// Lowercase MIME type
const mimeType = mediaType.shift()?.toLowerCase() ?? ""
const attributes = mediaType
.map((attribute: string) => {
let [key, value = ""] = attribute.split("=").map((s: string) => s.trim())
// Lowercase `charset`
if (key === 'charset') {
value = value.toLowerCase()
// Lowercase `charset`
if (key === "charset") {
value = value.toLowerCase()
if (value === DATA_URL_DEFAULT_CHARSET) {
return ''
}
}
if (value === DATA_URL_DEFAULT_CHARSET) {
return ""
}
}
return `${key}${value ? `=${value}` : ''}`
})
.filter(Boolean)
return `${key}${value ? `=${value}` : ""}`
})
.filter(Boolean)
const normalizedMediaType = [
...attributes,
]
const normalizedMediaType = [...attributes]
if (isBase64) {
normalizedMediaType.push('base64')
}
if (isBase64) {
normalizedMediaType.push("base64")
}
if (normalizedMediaType.length > 0 || (mimeType && mimeType !== DATA_URL_DEFAULT_MIME_TYPE)) {
normalizedMediaType.unshift(mimeType)
}
if (normalizedMediaType.length > 0 || (mimeType && mimeType !== DATA_URL_DEFAULT_MIME_TYPE)) {
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 {
const options = {
defaultProtocol: 'http',
normalizeProtocol: true,
forceHttp: false,
forceHttps: false,
stripAuthentication: true,
stripHash: false,
stripTextFragment: true,
stripWWW: true,
removeQueryParameters: [/^utm_\w+/i],
removeTrailingSlash: true,
removeSingleSlash: true,
removeDirectoryIndex: false,
removeExplicitPort: false,
sortQueryParameters: true,
...opts,
}
const options = {
defaultProtocol: "http",
normalizeProtocol: true,
forceHttp: false,
forceHttps: false,
stripAuthentication: true,
stripHash: false,
stripTextFragment: true,
stripWWW: true,
removeQueryParameters: [/^utm_\w+/i],
removeTrailingSlash: true,
removeSingleSlash: true,
removeDirectoryIndex: false,
removeExplicitPort: false,
sortQueryParameters: true,
...opts,
}
// Legacy: Append `:` to the protocol if missing.
if (typeof options.defaultProtocol === 'string' && !options.defaultProtocol.endsWith(':')) {
options.defaultProtocol = `${options.defaultProtocol}:`
}
// Legacy: Append `:` to the protocol if missing.
if (typeof options.defaultProtocol === "string" && !options.defaultProtocol.endsWith(":")) {
options.defaultProtocol = `${options.defaultProtocol}:`
}
urlString = urlString.trim()
urlString = urlString.trim()
// Data URL
if (/^data:/i.test(urlString)) {
return normalizeDataURL(urlString, options)
}
// Data URL
if (/^data:/i.test(urlString)) {
return normalizeDataURL(urlString, options)
}
if (hasCustomProtocol(urlString)) {
return urlString
}
if (hasCustomProtocol(urlString)) {
return urlString
}
const hasRelativeProtocol = urlString.startsWith('//')
const isRelativeUrl = !hasRelativeProtocol && /^\.*\//.test(urlString)
const hasRelativeProtocol = urlString.startsWith("//")
const isRelativeUrl = !hasRelativeProtocol && /^\.*\//.test(urlString)
// Prepend protocol
if (!isRelativeUrl) {
urlString = urlString.replace(/^(?!(?:\w+:)?\/\/)|^\/\//, options.defaultProtocol)
}
// Prepend protocol
if (!isRelativeUrl) {
urlString = urlString.replace(/^(?!(?:\w+:)?\/\/)|^\/\//, options.defaultProtocol)
}
const urlObject = new URL(urlString)
const urlObject = new URL(urlString)
if (options.forceHttp && options.forceHttps) {
throw new Error('The `forceHttp` and `forceHttps` options cannot be used together')
}
if (options.forceHttp && options.forceHttps) {
throw new Error("The `forceHttp` and `forceHttps` options cannot be used together")
}
if (options.forceHttp && urlObject.protocol === 'https:') {
urlObject.protocol = 'http:'
}
if (options.forceHttp && urlObject.protocol === "https:") {
urlObject.protocol = "http:"
}
if (options.forceHttps && urlObject.protocol === 'http:') {
urlObject.protocol = 'https:'
}
if (options.forceHttps && urlObject.protocol === "http:") {
urlObject.protocol = "https:"
}
// Remove auth
if (options.stripAuthentication) {
urlObject.username = ''
urlObject.password = ''
}
// Remove auth
if (options.stripAuthentication) {
urlObject.username = ""
urlObject.password = ""
}
// Remove hash
if (options.stripHash) {
urlObject.hash = ''
} else if (options.stripTextFragment) {
urlObject.hash = urlObject.hash.replace(/#?:~:text.*?$/i, '')
}
// Remove hash
if (options.stripHash) {
urlObject.hash = ""
} else if (options.stripTextFragment) {
urlObject.hash = urlObject.hash.replace(/#?:~:text.*?$/i, "")
}
// Remove duplicate slashes if not preceded by a protocol
// NOTE: This could be implemented using a single negative lookbehind
// regex, but we avoid that to maintain compatibility with older js engines
// which do not have support for that feature.
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.
// Remove duplicate slashes if not preceded by a protocol
// NOTE: This could be implemented using a single negative lookbehind
// regex, but we avoid that to maintain compatibility with older js engines
// which do not have support for that feature.
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.
// Split the string by occurrences of this protocol regex, and perform
// duplicate-slash replacement on the strings between those occurrences
// (if any).
const protocolRegex = /\b[a-z][a-z\d+\-.]{1,50}:\/\//g
// Split the string by occurrences of this protocol regex, and perform
// duplicate-slash replacement on the strings between those occurrences
// (if any).
const protocolRegex = /\b[a-z][a-z\d+\-.]{1,50}:\/\//g
let lastIndex = 0
let result = ''
for (;;) {
const match = protocolRegex.exec(urlObject.pathname)
if (!match) {
break
}
let lastIndex = 0
let result = ""
for (;;) {
const match = protocolRegex.exec(urlObject.pathname)
if (!match) {
break
}
const protocol = match[0]
const protocolAtIndex = match.index
const intermediate = urlObject.pathname.slice(lastIndex, protocolAtIndex)
const protocol = match[0]
const protocolAtIndex = match.index
const intermediate = urlObject.pathname.slice(lastIndex, protocolAtIndex)
result += intermediate.replace(/\/{2,}/g, '/')
result += protocol
lastIndex = protocolAtIndex + protocol.length
}
result += intermediate.replace(/\/{2,}/g, "/")
result += protocol
lastIndex = protocolAtIndex + protocol.length
}
const remnant = urlObject.pathname.slice(lastIndex, urlObject.pathname.length)
result += remnant.replace(/\/{2,}/g, '/')
const remnant = urlObject.pathname.slice(lastIndex, urlObject.pathname.length)
result += remnant.replace(/\/{2,}/g, "/")
urlObject.pathname = result
}
urlObject.pathname = result
}
// Decode URI octets
if (urlObject.pathname) {
try {
urlObject.pathname = decodeURI(urlObject.pathname)
} catch {}
}
// Decode URI octets
if (urlObject.pathname) {
try {
urlObject.pathname = decodeURI(urlObject.pathname)
} catch {}
}
// Remove directory index
if (options.removeDirectoryIndex === true) {
options.removeDirectoryIndex = [/^index\.[a-z]+$/]
}
// Remove directory index
if (options.removeDirectoryIndex === true) {
options.removeDirectoryIndex = [/^index\.[a-z]+$/]
}
if (Array.isArray(options.removeDirectoryIndex) && options.removeDirectoryIndex.length > 0) {
let pathComponents = urlObject.pathname.split('/')
const lastComponent = pathComponents[pathComponents.length - 1]
if (Array.isArray(options.removeDirectoryIndex) && options.removeDirectoryIndex.length > 0) {
let pathComponents = urlObject.pathname.split("/")
const lastComponent = pathComponents[pathComponents.length - 1]
if (testParameter(lastComponent, options.removeDirectoryIndex)) {
pathComponents = pathComponents.slice(0, -1)
urlObject.pathname = pathComponents.slice(1).join('/') + '/'
}
}
if (testParameter(lastComponent, options.removeDirectoryIndex)) {
pathComponents = pathComponents.slice(0, -1)
urlObject.pathname = pathComponents.slice(1).join("/") + "/"
}
}
if (urlObject.hostname) {
// Remove trailing dot
urlObject.hostname = urlObject.hostname.replace(/\.$/, '')
if (urlObject.hostname) {
// Remove trailing dot
urlObject.hostname = urlObject.hostname.replace(/\.$/, "")
// Remove `www.`
if (options.stripWWW && /^www\.(?!www\.)[a-z\-\d]{1,63}\.[a-z.\-\d]{2,63}$/.test(urlObject.hostname)) {
// Each label should be max 63 at length (min: 1).
// 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 `www.`
if (
options.stripWWW &&
/^www\.(?!www\.)[a-z\-\d]{1,63}\.[a-z.\-\d]{2,63}$/.test(urlObject.hostname)
) {
// Each label should be max 63 at length (min: 1).
// 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
if (Array.isArray(options.removeQueryParameters)) {
// @ts-ignore
for (const key of [...urlObject.searchParams.keys()]) {
if (testParameter(key, options.removeQueryParameters)) {
urlObject.searchParams.delete(key)
}
}
}
// Remove query unwanted parameters
if (Array.isArray(options.removeQueryParameters)) {
// @ts-ignore
for (const key of [...urlObject.searchParams.keys()]) {
if (testParameter(key, options.removeQueryParameters)) {
urlObject.searchParams.delete(key)
}
}
}
if (!Array.isArray(options.keepQueryParameters) && options.removeQueryParameters === true) {
urlObject.search = ''
}
if (!Array.isArray(options.keepQueryParameters) && options.removeQueryParameters === true) {
urlObject.search = ""
}
// Keep wanted query parameters
if (Array.isArray(options.keepQueryParameters) && options.keepQueryParameters.length > 0) {
// @ts-ignore
for (const key of [...urlObject.searchParams.keys()]) {
if (!testParameter(key, options.keepQueryParameters)) {
urlObject.searchParams.delete(key)
}
}
}
// Keep wanted query parameters
if (Array.isArray(options.keepQueryParameters) && options.keepQueryParameters.length > 0) {
// @ts-ignore
for (const key of [...urlObject.searchParams.keys()]) {
if (!testParameter(key, options.keepQueryParameters)) {
urlObject.searchParams.delete(key)
}
}
}
// Sort query parameters
if (options.sortQueryParameters) {
urlObject.searchParams.sort()
// Sort query parameters
if (options.sortQueryParameters) {
urlObject.searchParams.sort()
// Calling `.sort()` encodes the search parameters, so we need to decode them again.
try {
urlObject.search = decodeURIComponent(urlObject.search)
} catch {}
}
// Calling `.sort()` encodes the search parameters, so we need to decode them again.
try {
urlObject.search = decodeURIComponent(urlObject.search)
} catch {}
}
if (options.removeTrailingSlash) {
urlObject.pathname = urlObject.pathname.replace(/\/$/, '')
}
if (options.removeTrailingSlash) {
urlObject.pathname = urlObject.pathname.replace(/\/$/, "")
}
// Remove an explicit port number, excluding a default port number, if applicable
if (options.removeExplicitPort && urlObject.port) {
urlObject.port = ''
}
// Remove an explicit port number, excluding a default port number, if applicable
if (options.removeExplicitPort && urlObject.port) {
urlObject.port = ""
}
const oldUrlString = urlString
const oldUrlString = urlString
// Take advantage of many of the Node `url` normalizations
urlString = urlObject.toString()
// Take advantage of many of the Node `url` normalizations
urlString = urlObject.toString()
if (!options.removeSingleSlash && urlObject.pathname === '/' && !oldUrlString.endsWith('/') && urlObject.hash === '') {
urlString = urlString.replace(/\/$/, '')
}
if (
!options.removeSingleSlash &&
urlObject.pathname === "/" &&
!oldUrlString.endsWith("/") &&
urlObject.hash === ""
) {
urlString = urlString.replace(/\/$/, "")
}
// Remove ending `/` unless removeSingleSlash is false
if ((options.removeTrailingSlash || urlObject.pathname === '/') && urlObject.hash === '' && options.removeSingleSlash) {
urlString = urlString.replace(/\/$/, '')
}
// Remove ending `/` unless removeSingleSlash is false
if (
(options.removeTrailingSlash || urlObject.pathname === "/") &&
urlObject.hash === "" &&
options.removeSingleSlash
) {
urlString = urlString.replace(/\/$/, "")
}
// Restore relative protocol, if applicable
if (hasRelativeProtocol && !options.normalizeProtocol) {
urlString = urlString.replace(/^http:\/\//, '//')
}
// Restore relative protocol, if applicable
if (hasRelativeProtocol && !options.normalizeProtocol) {
urlString = urlString.replace(/^http:\/\//, "//")
}
// Remove http/https
if (options.stripProtocol) {
urlString = urlString.replace(/^(?:https?:)?\/\//, '')
}
// Remove http/https
if (options.stripProtocol) {
urlString = urlString.replace(/^(?:https?:)?\/\//, "")
}
return urlString
return urlString
}
+22 -20
View File
@@ -1,13 +1,12 @@
import EventEmitter from "events"
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest"
import { isRelayUrl } from "@welshman/util"
import { LocalRelay, Repository, LOCAL_RELAY_URL } from "@welshman/relay"
import { AdapterEvent, SocketAdapter, LocalAdapter, getAdapter } from "../src/adapter"
import { ClientMessage, RelayMessage } from "../src/message"
import { Socket, SocketEvent } from "../src/socket"
import { Pool } from "../src/pool"
import {describe, expect, it, vi, beforeEach, afterEach} from "vitest"
import {LocalRelay, Repository, LOCAL_RELAY_URL} from "@welshman/relay"
import {AdapterEvent, SocketAdapter, LocalAdapter, getAdapter} from "../src/adapter"
import {ClientMessage, RelayMessage} from "../src/message"
import {Socket, SocketEvent} from "../src/socket"
import {Pool} from "../src/pool"
vi.mock('isomorphic-ws', () => {
vi.mock("isomorphic-ws", () => {
const WebSocket = vi.fn(function (this: any) {
setTimeout(() => this.onopen())
})
@@ -18,7 +17,7 @@ vi.mock('isomorphic-ws', () => {
this.onclose()
})
return { default: WebSocket }
return {default: WebSocket}
})
describe("SocketAdapter", () => {
@@ -27,7 +26,7 @@ describe("SocketAdapter", () => {
beforeEach(() => {
vi.useFakeTimers()
socket = new Socket('wss://test.relay')
socket = new Socket("wss://test.relay")
adapter = new SocketAdapter(socket)
})
@@ -48,15 +47,15 @@ describe("SocketAdapter", () => {
const receiveSpy = vi.fn()
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")
expect(receiveSpy).toHaveBeenCalledWith(message, "wss://test.relay")
})
it("should send messages to socket", () => {
const sendSpy = vi.spyOn(socket, 'send')
const message: ClientMessage = ["EVENT", { id: "123", kind: 1 }]
const sendSpy = vi.spyOn(socket, "send")
const message: ClientMessage = ["EVENT", {id: "123", kind: 1}]
adapter.send(message)
expect(sendSpy).toHaveBeenCalledWith(message)
@@ -77,7 +76,7 @@ describe("LocalAdapter", () => {
const mockRelay = new EventEmitter()
Object.assign(mockRelay, {
send: vi.fn(),
removeAllListeners: vi.fn()
removeAllListeners: vi.fn(),
})
relay = mockRelay as unknown as LocalRelay & EventEmitter
adapter = new LocalAdapter(relay)
@@ -98,14 +97,14 @@ describe("LocalAdapter", () => {
const receiveSpy = vi.fn()
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)
expect(receiveSpy).toHaveBeenCalledWith(message, LOCAL_RELAY_URL)
})
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)
expect(relay.send).toHaveBeenCalledWith("EVENT", message[1])
@@ -136,13 +135,13 @@ describe("getAdapter", () => {
it("should return LocalAdapter for local relay URL", () => {
const url = LOCAL_RELAY_URL
const adapter = getAdapter(url, { repository })
const adapter = getAdapter(url, {repository})
expect(adapter).toBeInstanceOf(LocalAdapter)
})
it("should return SocketAdapter for remote relay URL", () => {
const url = "wss://test.relay"
const adapter = getAdapter(url, { pool })
const adapter = getAdapter(url, {pool})
expect(adapter).toBeInstanceOf(SocketAdapter)
})
@@ -151,9 +150,12 @@ describe("getAdapter", () => {
const getCustomAdapter = vi.fn().mockReturnValue(customAdapter)
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)
})
})
+14 -15
View File
@@ -1,12 +1,11 @@
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest"
import { Socket, SocketStatus, SocketEvent } from "../src/socket"
import { makeEvent, StampedEvent, CLIENT_AUTH } from "@welshman/util"
import { Nip01Signer } from "@welshman/signer"
import { AuthState, AuthStatus, AuthStateEvent, makeAuthEvent } from "../src/auth"
import EventEmitter from "events"
import { RelayMessage } from "../src/message"
import {describe, expect, it, vi, beforeEach, afterEach} from "vitest"
import {Socket, SocketStatus, SocketEvent} from "../src/socket"
import {StampedEvent, CLIENT_AUTH} from "@welshman/util"
import {Nip01Signer} from "@welshman/signer"
import {AuthStatus, AuthStateEvent} from "../src/auth"
import {RelayMessage} from "../src/message"
vi.mock('isomorphic-ws', () => {
vi.mock("isomorphic-ws", () => {
const WebSocket = vi.fn(function (this: any) {
setTimeout(() => this.onopen())
})
@@ -17,14 +16,14 @@ vi.mock('isomorphic-ws', () => {
this.onclose()
})
return { default: WebSocket }
return {default: WebSocket}
})
describe('auth', () => {
describe("auth", () => {
let socket: Socket
beforeEach(() => {
socket = new Socket('wss://test.relay')
socket = new Socket("wss://test.relay")
})
afterEach(() => {
@@ -72,7 +71,7 @@ describe('auth', () => {
})
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)
expect(socket.auth.status).toBe(AuthStatus.PendingResponse)
@@ -113,7 +112,7 @@ describe('auth', () => {
const sign = vi.fn()
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
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 () => {
const sendSpy = vi.spyOn(socket, 'send')
const sendSpy = vi.spyOn(socket, "send")
let event
socket.auth.challenge = "challenge123"
+58 -49
View File
@@ -1,14 +1,14 @@
import { AUTH_JOIN } from "@welshman/util"
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest"
import { Socket, SocketStatus, SocketEvent } from "../src/socket"
import { AuthStatus, AuthStateEvent } from "../src/auth"
import {AUTH_JOIN} from "@welshman/util"
import {describe, expect, it, vi, beforeEach, afterEach} from "vitest"
import {Socket, SocketStatus, SocketEvent} from "../src/socket"
import {AuthStatus, AuthStateEvent} from "../src/auth"
import {
socketPolicyAuthBuffer,
socketPolicyConnectOnSend,
socketPolicyCloseOnTimeout,
socketPolicyReopenActive
socketPolicyReopenActive,
} from "../src/policy"
import { ClientMessage, RelayMessage } from "../src/message"
import {ClientMessage, RelayMessage} from "../src/message"
// Hoist mock definition to top level
const mockWs = vi.hoisted(() => ({
@@ -21,11 +21,11 @@ const mockWs = vi.hoisted(() => ({
}))
// Mock the WebSocket module
vi.mock('isomorphic-ws', () => ({
default: mockWs
vi.mock("isomorphic-ws", () => ({
default: mockWs,
}))
describe('policy', () => {
describe("policy", () => {
let socket: Socket
beforeEach(() => {
@@ -42,22 +42,22 @@ describe('policy', () => {
describe("socketPolicyAuthBuffer", () => {
it("should buffer messages when not authenticated", () => {
const cleanup = socketPolicyAuthBuffer(socket)
const sendSpy = vi.spyOn(socket, 'send')
const sendSpy = vi.spyOn(socket, "send")
socket.emit(SocketEvent.Receive, ["AUTH", "challenge"])
// Regular event should be buffered
const event: ClientMessage = ["EVENT", { id: "123"}]
const event: ClientMessage = ["EVENT", {id: "123"}]
socket.send(event)
expect(sendSpy).toHaveBeenCalledWith(event)
// Auth event should not be buffered
const authEvent: ClientMessage = ["AUTH", { id: "456" }]
const authEvent: ClientMessage = ["AUTH", {id: "456"}]
socket.send(authEvent)
expect(sendSpy).toHaveBeenCalledWith(authEvent)
// 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)
expect(sendSpy).toHaveBeenCalledWith(joinEvent)
@@ -66,18 +66,18 @@ describe('policy', () => {
it("should send buffered messages when auth succeeds", () => {
const cleanup = socketPolicyAuthBuffer(socket)
const sendSpy = vi.spyOn(socket, 'send')
const sendSpy = vi.spyOn(socket, "send")
socket.emit(SocketEvent.Receive, ["AUTH", "challenge"])
// Buffer some messages
const event1: ClientMessage = ["EVENT", { id: "123"}]
const event2: ClientMessage = ["EVENT", { id: "456"}]
const event1: ClientMessage = ["EVENT", {id: "123"}]
const event2: ClientMessage = ["EVENT", {id: "456"}]
socket.send(event1)
socket.send(event2)
// Auth succeeds
socket.send(["AUTH", { id: "auth" }])
socket.send(["AUTH", {id: "auth"}])
socket.emit(AuthStateEvent.Status, AuthStatus.Ok)
expect(sendSpy).toHaveBeenCalledWith(event1)
@@ -88,12 +88,12 @@ describe('policy', () => {
it("should handle CLOSE messages properly", () => {
const cleanup = socketPolicyAuthBuffer(socket)
const sendSpy = vi.spyOn(socket, 'send')
const sendSpy = vi.spyOn(socket, "send")
socket.emit(SocketEvent.Receive, ["AUTH", "challenge"])
// Buffer a REQ message
const req: ClientMessage = ["REQ", "123", { kinds: [1] }]
const req: ClientMessage = ["REQ", "123", {kinds: [1]}]
socket.send(req)
// Send CLOSE for buffered REQ
@@ -109,10 +109,13 @@ describe('policy', () => {
it("should retry events once when auth-required", () => {
const cleanup = socketPolicyAuthBuffer(socket)
const recvQueueRemoveSpy = vi.spyOn(socket._recvQueue, 'remove')
const recvQueueRemoveSpy = vi.spyOn(socket._recvQueue, "remove")
// 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)
// Receive auth-required rejection
@@ -134,10 +137,10 @@ describe('policy', () => {
it("should retry REQ once when auth-required", () => {
const cleanup = socketPolicyAuthBuffer(socket)
const recvQueueRemoveSpy = vi.spyOn(socket._recvQueue, 'remove')
const recvQueueRemoveSpy = vi.spyOn(socket._recvQueue, "remove")
// Send a REQ
const req: ClientMessage = ["REQ", "123", { kinds: [1] }]
const req: ClientMessage = ["REQ", "123", {kinds: [1]}]
socket.emit(SocketEvent.Send, req)
// Receive auth-required rejection
@@ -159,10 +162,13 @@ describe('policy', () => {
it("should not retry AUTH_JOIN events", () => {
const cleanup = socketPolicyAuthBuffer(socket)
const sendSpy = vi.spyOn(socket, 'send')
const sendSpy = vi.spyOn(socket, "send")
// 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)
// Receive auth-required rejection
@@ -176,10 +182,13 @@ describe('policy', () => {
it("should clear pending messages on successful response", () => {
const cleanup = socketPolicyAuthBuffer(socket)
const sendSpy = vi.spyOn(socket, 'send')
const sendSpy = vi.spyOn(socket, "send")
// 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)
// Receive successful response
@@ -198,13 +207,13 @@ describe('policy', () => {
describe("socketPolicyConnectOnSend", () => {
it("should open socket on send when closed", () => {
const cleanup = socketPolicyConnectOnSend(socket)
const openSpy = vi.spyOn(socket, 'open')
const openSpy = vi.spyOn(socket, "open")
// Socket starts closed
socket.emit(SocketEvent.Status, SocketStatus.Closed)
// Send a message
const event: ClientMessage = ["EVENT", { id: "123", kind: 1 }]
const event: ClientMessage = ["EVENT", {id: "123", kind: 1}]
socket.emit(SocketEvent.Sending, event)
// Should open the socket
@@ -215,13 +224,13 @@ describe('policy', () => {
it("should not open socket if already open", () => {
const cleanup = socketPolicyConnectOnSend(socket)
const openSpy = vi.spyOn(socket, 'open')
const openSpy = vi.spyOn(socket, "open")
// Socket is open
socket.emit(SocketEvent.Status, SocketStatus.Open)
// Send a message
const event: ClientMessage = ["EVENT", { id: "123", kind: 1 }]
const event: ClientMessage = ["EVENT", {id: "123", kind: 1}]
socket.emit(SocketEvent.Sending, event)
// Should not try to open the socket
@@ -232,14 +241,14 @@ describe('policy', () => {
it("should not open socket if there was a recent error", () => {
const cleanup = socketPolicyConnectOnSend(socket)
const openSpy = vi.spyOn(socket, 'open')
const openSpy = vi.spyOn(socket, "open")
// Socket has an error
socket.emit(SocketEvent.Status, SocketStatus.Error)
socket.emit(SocketEvent.Status, SocketStatus.Closed)
// Send a message
const event: ClientMessage = ["EVENT", { id: "123", kind: 1 }]
const event: ClientMessage = ["EVENT", {id: "123", kind: 1}]
socket.emit(SocketEvent.Sending, event)
// Should not try to open the socket due to recent error
@@ -261,7 +270,7 @@ describe('policy', () => {
describe("socketPolicyCloseOnTimeout", () => {
it("should close socket after 30 seconds of inactivity", async () => {
const cleanup = socketPolicyCloseOnTimeout(socket)
const closeSpy = vi.spyOn(socket, 'close')
const closeSpy = vi.spyOn(socket, "close")
// Set socket as open
socket.emit(SocketEvent.Status, SocketStatus.Open)
@@ -277,7 +286,7 @@ describe('policy', () => {
it("should reset timer on send activity", () => {
const cleanup = socketPolicyCloseOnTimeout(socket)
const closeSpy = vi.spyOn(socket, 'close')
const closeSpy = vi.spyOn(socket, "close")
// Set socket as open
socket.emit(SocketEvent.Status, SocketStatus.Open)
@@ -286,7 +295,7 @@ describe('policy', () => {
vi.advanceTimersByTime(20000)
// Send a message
socket.emit(SocketEvent.Send, ["EVENT", { id: "123" }])
socket.emit(SocketEvent.Send, ["EVENT", {id: "123"}])
// Advance time partially again
vi.advanceTimersByTime(20000)
@@ -305,7 +314,7 @@ describe('policy', () => {
it("should reset timer on receive activity", () => {
const cleanup = socketPolicyCloseOnTimeout(socket)
const closeSpy = vi.spyOn(socket, 'close')
const closeSpy = vi.spyOn(socket, "close")
// Set socket as open
socket.emit(SocketEvent.Status, SocketStatus.Open)
@@ -314,7 +323,7 @@ describe('policy', () => {
vi.advanceTimersByTime(20000)
// Receive a message
socket.emit(SocketEvent.Receive, ["EVENT", "123", { id: "123" }])
socket.emit(SocketEvent.Receive, ["EVENT", "123", {id: "123"}])
// Advance time partially again
vi.advanceTimersByTime(20000)
@@ -333,7 +342,7 @@ describe('policy', () => {
it("should not close socket if not open", () => {
const cleanup = socketPolicyCloseOnTimeout(socket)
const closeSpy = vi.spyOn(socket, 'close')
const closeSpy = vi.spyOn(socket, "close")
// Set socket as closed
socket.emit(SocketEvent.Status, SocketStatus.Closed)
@@ -351,10 +360,10 @@ describe('policy', () => {
describe("socketPolicyReopenActive", () => {
it("should reopen socket when closed with pending messages", async () => {
const cleanup = socketPolicyReopenActive(socket)
const sendSpy = vi.spyOn(socket, 'send')
const sendSpy = vi.spyOn(socket, "send")
// 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 closes
@@ -371,10 +380,10 @@ describe('policy', () => {
it("should reopen socket when closed with pending requests", async () => {
const cleanup = socketPolicyReopenActive(socket)
const sendSpy = vi.spyOn(socket, 'send')
const sendSpy = vi.spyOn(socket, "send")
// 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 closes
@@ -391,10 +400,10 @@ describe('policy', () => {
it("should not reopen socket immediately after previous open", async () => {
const cleanup = socketPolicyReopenActive(socket)
const sendSpy = vi.spyOn(socket, 'send')
const sendSpy = vi.spyOn(socket, "send")
// 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 opens then closes quickly
@@ -418,10 +427,10 @@ describe('policy', () => {
it("should remove pending messages when they complete", () => {
const cleanup = socketPolicyReopenActive(socket)
const sendSpy = vi.spyOn(socket, 'send')
const sendSpy = vi.spyOn(socket, "send")
// 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)
// Event completes successfully
@@ -441,10 +450,10 @@ describe('policy', () => {
it("should remove pending messages when closed", () => {
const cleanup = socketPolicyReopenActive(socket)
const sendSpy = vi.spyOn(socket, 'send')
const sendSpy = vi.spyOn(socket, "send")
// 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)
// Send close for the request
+7 -8
View File
@@ -1,9 +1,8 @@
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest"
import { Socket } from "../src/socket"
import { Pool, makeSocket } from "../src/pool"
import { normalizeRelayUrl } from "@welshman/util"
import {describe, expect, it, vi, beforeEach, afterEach} from "vitest"
import {Socket} from "../src/socket"
import {Pool} from "../src/pool"
vi.mock('isomorphic-ws', () => {
vi.mock("isomorphic-ws", () => {
const WebSocket = vi.fn(function (this: any) {
setTimeout(() => this.onopen())
})
@@ -14,7 +13,7 @@ vi.mock('isomorphic-ws', () => {
this.onclose()
})
return { default: WebSocket }
return {default: WebSocket}
})
describe("Pool", () => {
@@ -95,7 +94,7 @@ describe("Pool", () => {
describe("remove", () => {
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.remove(mockSocket.url)
@@ -113,7 +112,7 @@ describe("Pool", () => {
describe("clear", () => {
it("should remove all sockets", () => {
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) {
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 { EventEmitter } from "events"
import { SinglePublish, MultiPublish, PublishEvent, PublishStatus, } from "../src/publish"
import { AbstractAdapter, AdapterEvent, MockAdapter } from "../src/adapter"
import { ClientMessageType, RelayMessage } from "../src/message"
import { SignedEvent, makeEvent } from "@welshman/util"
import { Nip01Signer } from '@welshman/signer'
import {describe, expect, it, vi, beforeEach, afterEach} from "vitest"
import {SinglePublish, MultiPublish, PublishEvent} from "../src/publish"
import {MockAdapter} from "../src/adapter"
import {ClientMessageType} from "../src/message"
import {makeEvent} from "@welshman/util"
import {Nip01Signer} from "@welshman/signer"
describe("SinglePublish", () => {
beforeEach(() => {
@@ -17,12 +16,12 @@ describe("SinglePublish", () => {
it("success works", async () => {
const sendSpy = vi.fn()
const adapter = new MockAdapter('1', sendSpy)
const adapter = new MockAdapter("1", sendSpy)
const signer = Nip01Signer.ephemeral()
const event = await signer.sign(makeEvent(1))
const pub = new SinglePublish({
relay: '1',
relay: "1",
context: {getAdapter: () => adapter},
event,
})
@@ -50,12 +49,12 @@ describe("SinglePublish", () => {
it("failure works", async () => {
const sendSpy = vi.fn()
const adapter = new MockAdapter('1', sendSpy)
const adapter = new MockAdapter("1", sendSpy)
const signer = Nip01Signer.ephemeral()
const event = await signer.sign(makeEvent(1))
const pub = new SinglePublish({
relay: '1',
relay: "1",
context: {getAdapter: () => adapter},
event,
})
@@ -83,12 +82,12 @@ describe("SinglePublish", () => {
it("timeout works", async () => {
const sendSpy = vi.fn()
const adapter = new MockAdapter('1', sendSpy)
const adapter = new MockAdapter("1", sendSpy)
const signer = Nip01Signer.ephemeral()
const event = await signer.sign(makeEvent(1))
const pub = new SinglePublish({
relay: '1',
relay: "1",
context: {getAdapter: () => adapter},
event,
})
@@ -117,12 +116,12 @@ describe("SinglePublish", () => {
it("abort works", async () => {
const sendSpy = vi.fn()
const adapter = new MockAdapter('1', sendSpy)
const adapter = new MockAdapter("1", sendSpy)
const signer = Nip01Signer.ephemeral()
const event = await signer.sign(makeEvent(1))
const pub = new SinglePublish({
relay: '1',
relay: "1",
context: {getAdapter: () => adapter},
event,
})
@@ -163,27 +162,31 @@ describe("MultiPublish", () => {
it("should all basically work", async () => {
const send1Spy = vi.fn()
const adapter1 = new MockAdapter('1', send1Spy)
const adapter1 = new MockAdapter("1", send1Spy)
const send2Spy = vi.fn()
const adapter2 = new MockAdapter('2', send2Spy)
const adapter2 = new MockAdapter("2", send2Spy)
const send3Spy = vi.fn()
const adapter3 = new MockAdapter('3', send3Spy)
const adapter3 = new MockAdapter("3", send3Spy)
const signer = Nip01Signer.ephemeral()
const event = await signer.sign(makeEvent(1))
const pub = new MultiPublish({
event,
relays: ['1', '2', '3'],
relays: ["1", "2", "3"],
context: {
getAdapter: (url: string) => {
switch(url) {
case '1': return adapter1
case '2': return adapter2
case '3': return adapter3
default: throw new Error(`Unknown relay: ${url}`)
switch (url) {
case "1":
return adapter1
case "2":
return adapter2
case "3":
return adapter3
default:
throw new Error(`Unknown relay: ${url}`)
}
},
}
},
})
const successSpy = vi.fn()
@@ -199,7 +202,6 @@ describe("MultiPublish", () => {
adapter1.receive(["OK", event.id, true, "hi"])
adapter2.receive(["OK", event.id, false, "hi"])
await vi.runAllTimers()
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 { Nip01Signer } from '@welshman/signer'
import { makeEvent } from '@welshman/util'
import { ClientMessageType } from "../src/message"
import { MockAdapter } from "../src/adapter"
import { SingleRequest, MultiRequest, RequestEvent } from "../src/request"
import {describe, expect, it, vi, beforeEach, afterEach} from "vitest"
import {Nip01Signer} from "@welshman/signer"
import {makeEvent} from "@welshman/util"
import {ClientMessageType} from "../src/message"
import {MockAdapter} from "../src/adapter"
import {SingleRequest, MultiRequest, RequestEvent} from "../src/request"
describe("SingleRequest", () => {
beforeEach(() => {
@@ -16,9 +16,9 @@ describe("SingleRequest", () => {
it("everything basically works", async () => {
const sendSpy = vi.fn()
const adapter = new MockAdapter('1', sendSpy)
const adapter = new MockAdapter("1", sendSpy)
const req = new SingleRequest({
relay: 'whatever',
relay: "whatever",
filters: [{kinds: [1]}],
context: {getAdapter: () => adapter},
})
@@ -82,14 +82,14 @@ describe("MultiRequest", () => {
it("everything basically works", async () => {
const send1Spy = vi.fn()
const adapter1 = new MockAdapter('1', send1Spy)
const adapter1 = new MockAdapter("1", send1Spy)
const send2Spy = vi.fn()
const adapter2 = new MockAdapter('2', send2Spy)
const adapter2 = new MockAdapter("2", send2Spy)
const req = new MultiRequest({
relays: ['1', '2'],
relays: ["1", "2"],
filters: [{kinds: [1]}],
context: {
getAdapter: (url: string) => url === '1' ? adapter1 : adapter2
getAdapter: (url: string) => (url === "1" ? adapter1 : adapter2),
},
})
@@ -129,10 +129,10 @@ describe("MultiRequest", () => {
await vi.runAllTimersAsync()
expect(duplicateSpy).toHaveBeenCalledWith(event1, '2')
expect(filteredSpy).toHaveBeenCalledWith(event2, '1')
expect(invalidSpy).toHaveBeenCalledWith(event3, '1')
expect(eventSpy).toHaveBeenCalledWith(event1, '1')
expect(duplicateSpy).toHaveBeenCalledWith(event1, "2")
expect(filteredSpy).toHaveBeenCalledWith(event2, "1")
expect(invalidSpy).toHaveBeenCalledWith(event3, "1")
expect(eventSpy).toHaveBeenCalledWith(event1, "1")
expect(eoseSpy).toHaveBeenCalledTimes(0)
adapter1.receive(["EOSE", id1])
@@ -145,4 +145,3 @@ describe("MultiRequest", () => {
expect(closeSpy).toHaveBeenCalledTimes(1)
})
})
+12 -11
View File
@@ -1,10 +1,9 @@
import { sleep } from "@welshman/lib"
import WebSocket from 'isomorphic-ws'
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
import { Socket, SocketStatus, SocketEvent } from "../src/socket"
import { ClientMessage, RelayMessage } from "../src/message"
import WebSocket from "isomorphic-ws"
import {afterEach, beforeEach, describe, expect, it, vi} from "vitest"
import {Socket, SocketStatus, SocketEvent} from "../src/socket"
import {ClientMessage, RelayMessage} from "../src/message"
vi.mock('isomorphic-ws', () => {
vi.mock("isomorphic-ws", () => {
const WebSocket = vi.fn(function (this: any) {
setTimeout(() => this.onopen())
})
@@ -15,7 +14,7 @@ vi.mock('isomorphic-ws', () => {
this.onclose()
})
return { default: WebSocket }
return {default: WebSocket}
})
describe("Socket", () => {
@@ -77,9 +76,11 @@ describe("Socket", () => {
socket.open()
const closeSpy = vi.spyOn(socket._ws, "close")
socket.close()
expect(socket._ws!.close).toHaveBeenCalled()
expect(closeSpy).toHaveBeenCalled()
expect(statusSpy).toHaveBeenCalledWith(SocketStatus.Closed, "wss://test.relay")
})
})
@@ -89,7 +90,7 @@ describe("Socket", () => {
const enqueueSpy = vi.fn()
socket.on(SocketEvent.Sending, enqueueSpy)
const message: ClientMessage = ["EVENT", { id: "123", kind: 1 }]
const message: ClientMessage = ["EVENT", {id: "123", kind: 1}]
socket.send(message)
expect(enqueueSpy).toHaveBeenCalledWith(message, "wss://test.relay")
@@ -102,7 +103,7 @@ describe("Socket", () => {
socket.open()
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)
await vi.runAllTimers()
@@ -118,7 +119,7 @@ describe("Socket", () => {
socket.on(SocketEvent.Receive, receiveSpy)
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)
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 {
ClientMessage,
@@ -7,7 +7,6 @@ import {
isClientEvent,
isClientReq,
isClientNegClose,
ClientMessageType,
RelayMessage,
isRelayOk,
isRelayEose,
@@ -50,7 +49,7 @@ export const socketPolicyAuthBuffer = (socket: Socket) => {
}),
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 (isRelayClosed(message) && message[2]?.startsWith('auth-required:')) {
if (isRelayClosed(message) && message[2]?.startsWith("auth-required:")) {
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 (isRelayOk(message) && !message[2] && message[3]?.startsWith('auth-required:')) {
if (isRelayOk(message) && !message[2] && message[3]?.startsWith("auth-required:")) {
socket._recvQueue.remove(message)
}
}),
+1 -1
View File
@@ -289,7 +289,7 @@ export const makeLoader = (options: LoaderOptions) =>
tracker,
relays: [relay],
autoClose: true,
...options
...options,
})
let count = 0
-1
View File
@@ -19,7 +19,6 @@ describe("LocalRelay", () => {
const id = "ff".repeat(32)
const sig = "00".repeat(64)
const currentTime = now()
const onionUrl = "abcdefghijklmnopqrstuvwxyz234567abcdefghijklmnopqrstuvwx.onion"
const createEvent = (overrides = {}): TrustedEvent => ({
id: id,
-23
View File
@@ -3,7 +3,6 @@ import {Repository} from "@welshman/relay"
import {get} from "svelte/store"
import {afterEach, beforeEach, describe, expect, it, vi} from "vitest"
import {
adapter,
custom,
deriveEvents,
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", () => {
const mockRepository = {
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
export type DeriveEventsMappedOptions<T> = {
+1 -1
View File
@@ -184,7 +184,7 @@ describe("Events", () => {
describe("signature validation", () => {
it("should validate signature using verifiedSymbol", () => {
let event = createSignedEvent() as Events.SignedEvent
const event = createSignedEvent() as Events.SignedEvent
event[verifiedSymbol] = true
expect(Events.verifyEvent(event)).toBe(true)
+1 -3
View File
@@ -211,9 +211,7 @@ describe("Filters", () => {
it("should calculate filter generality", () => {
expect(getFilterGenerality({ids: [id]})).toBe(0)
expect(getFilterGenerality({authors: [pubkey], "#p": [pubkey]})).toBe(0.2)
expect(getFilterGenerality({authors: [pubkey, pubkey, pubkey], kinds: [1]})).toBe(
0.01,
)
expect(getFilterGenerality({authors: [pubkey, pubkey, pubkey], kinds: [1]})).toBe(0.01)
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 =>