Lint
This commit is contained in:
@@ -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> = {}
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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)])
|
||||
|
||||
@@ -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,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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,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) =>
|
||||
|
||||
@@ -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]}]),
|
||||
})
|
||||
|
||||
@@ -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()})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,6 +1,5 @@
|
||||
import {writable, derived} from "svelte/store"
|
||||
import {Zapper} from "@welshman/util"
|
||||
import {MultiRequestOptions} from "@welshman/net"
|
||||
import {
|
||||
identity,
|
||||
fetchJson,
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)[]) => {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
+517
-490
File diff suppressed because it is too large
Load Diff
@@ -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)
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -289,7 +289,7 @@ export const makeLoader = (options: LoaderOptions) =>
|
||||
tracker,
|
||||
relays: [relay],
|
||||
autoClose: true,
|
||||
...options
|
||||
...options,
|
||||
})
|
||||
|
||||
let count = 0
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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> = {
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
|
||||
@@ -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 =>
|
||||
|
||||
Reference in New Issue
Block a user