Move net2 to net, update dvm
This commit is contained in:
@@ -1,40 +0,0 @@
|
||||
import {Connection, ConnectionStatus} from "../src/Connection"
|
||||
import {ConnectionEvent} from "../src/ConnectionEvent"
|
||||
import {vi, describe, it, expect, beforeEach, afterEach} from "vitest"
|
||||
|
||||
describe("Connection", () => {
|
||||
let connection: Connection
|
||||
|
||||
beforeEach(() => {
|
||||
connection = new Connection("wss://test.relay/")
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
connection.cleanup()
|
||||
})
|
||||
|
||||
it("should initialize with correct state", () => {
|
||||
expect(connection.status).toBe(ConnectionStatus.Open)
|
||||
expect(connection.url).toBe("wss://test.relay/")
|
||||
})
|
||||
|
||||
it("should emit events with connection instance", () => {
|
||||
const spy = vi.fn()
|
||||
connection.on(ConnectionEvent.Open, spy)
|
||||
connection.emit(ConnectionEvent.Open)
|
||||
expect(spy).toHaveBeenCalledWith(connection)
|
||||
})
|
||||
|
||||
it("should throw when sending message on closed connection", async () => {
|
||||
connection.close()
|
||||
await expect(connection.send(["EVENT", {}])).rejects.toThrow()
|
||||
})
|
||||
|
||||
it("should cleanup properly", () => {
|
||||
const spy = vi.fn()
|
||||
connection.on("test", spy)
|
||||
connection.cleanup()
|
||||
connection.emit("test" as any)
|
||||
expect(spy).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -1,261 +0,0 @@
|
||||
import {ConnectionAuth, AuthStatus, AuthMode} from "../src/ConnectionAuth"
|
||||
import {Connection} from "../src/Connection"
|
||||
import {ConnectionEvent} from "../src/ConnectionEvent"
|
||||
import {ctx, sleep} from "@welshman/lib"
|
||||
import {vi, describe, it, expect, beforeEach, afterEach} from "vitest"
|
||||
import {SocketStatus} from "../src/Socket"
|
||||
|
||||
describe("ConnectionAuth", () => {
|
||||
let connection: Connection
|
||||
let auth: ConnectionAuth
|
||||
let mockSignEvent: any
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
connection = new Connection("wss://test.relay/")
|
||||
// Mock socket operations
|
||||
connection.socket.open = vi.fn().mockResolvedValue(undefined)
|
||||
connection.socket.status = SocketStatus.Open
|
||||
connection.send = vi.fn().mockResolvedValue(undefined)
|
||||
|
||||
auth = connection.auth
|
||||
mockSignEvent = vi.fn()
|
||||
ctx.net = {...ctx.net, signEvent: mockSignEvent, authMode: AuthMode.Explicit}
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
describe("initialization", () => {
|
||||
it("should initialize with None status", () => {
|
||||
expect(auth.status).toBe(AuthStatus.None)
|
||||
expect(auth.challenge).toBeUndefined()
|
||||
expect(auth.request).toBeUndefined()
|
||||
expect(auth.message).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("message handling", () => {
|
||||
it("should handle AUTH message and set challenge", () => {
|
||||
connection.emit(ConnectionEvent.Receive, ["AUTH", "challenge123"])
|
||||
expect(auth.challenge).toBe("challenge123")
|
||||
expect(auth.status).toBe(AuthStatus.Requested)
|
||||
})
|
||||
|
||||
it("should ignore AUTH message if challenge matches current challenge", () => {
|
||||
auth.challenge = "challenge123"
|
||||
auth.status = AuthStatus.PendingResponse
|
||||
|
||||
connection.emit(ConnectionEvent.Receive, ["AUTH", "challenge123"])
|
||||
expect(auth.status).toBe(AuthStatus.PendingResponse)
|
||||
})
|
||||
|
||||
it("should handle successful OK message", () => {
|
||||
auth.challenge = "challenge123"
|
||||
auth.request = "request123"
|
||||
auth.status = AuthStatus.PendingResponse
|
||||
|
||||
connection.emit(ConnectionEvent.Receive, ["OK", "request123", true, "success"])
|
||||
expect(auth.status).toBe(AuthStatus.Ok)
|
||||
expect(auth.message).toBe("success")
|
||||
})
|
||||
|
||||
it("should handle failed OK message", () => {
|
||||
auth.challenge = "challenge123"
|
||||
auth.request = "request123"
|
||||
auth.status = AuthStatus.PendingResponse
|
||||
|
||||
connection.emit(ConnectionEvent.Receive, ["OK", "request123", false, "forbidden"])
|
||||
expect(auth.status).toBe(AuthStatus.Forbidden)
|
||||
expect(auth.message).toBe("forbidden")
|
||||
})
|
||||
|
||||
it("should ignore OK message for different request", () => {
|
||||
auth.challenge = "challenge123"
|
||||
auth.request = "request123"
|
||||
auth.status = AuthStatus.PendingResponse
|
||||
|
||||
connection.emit(ConnectionEvent.Receive, ["OK", "different123", true, "success"])
|
||||
expect(auth.status).toBe(AuthStatus.PendingResponse)
|
||||
expect(auth.message).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("connection close handling", () => {
|
||||
it("should reset state on connection close", () => {
|
||||
auth.challenge = "challenge123"
|
||||
auth.request = "request123"
|
||||
auth.message = "message"
|
||||
auth.status = AuthStatus.Ok
|
||||
|
||||
connection.emit(ConnectionEvent.Close)
|
||||
|
||||
expect(auth.challenge).toBeUndefined()
|
||||
expect(auth.request).toBeUndefined()
|
||||
expect(auth.message).toBeUndefined()
|
||||
expect(auth.status).toBe(AuthStatus.None)
|
||||
})
|
||||
})
|
||||
|
||||
describe("respond()", () => {
|
||||
it("should throw if no challenge exists", async () => {
|
||||
await expect(auth.respond()).rejects.toThrow("Attempted to authenticate with no challenge")
|
||||
})
|
||||
|
||||
it("should throw if status is not Requested", async () => {
|
||||
auth.challenge = "challenge123"
|
||||
auth.status = AuthStatus.Ok
|
||||
|
||||
await expect(auth.respond()).rejects.toThrow(
|
||||
"Attempted to authenticate when auth is already ok",
|
||||
)
|
||||
})
|
||||
|
||||
it("should handle successful signature", async () => {
|
||||
auth.challenge = "challenge123"
|
||||
auth.status = AuthStatus.Requested
|
||||
const signedEvent = {id: "event123" /* other event fields */}
|
||||
mockSignEvent.mockResolvedValue(signedEvent)
|
||||
|
||||
await auth.respond()
|
||||
|
||||
expect(auth.request).toBe("event123")
|
||||
expect(auth.status).toBe(AuthStatus.PendingResponse)
|
||||
expect(connection.send).toHaveBeenCalledWith(["AUTH", signedEvent])
|
||||
})
|
||||
|
||||
it("should handle denied signature", async () => {
|
||||
auth.challenge = "challenge123"
|
||||
auth.status = AuthStatus.Requested
|
||||
mockSignEvent.mockResolvedValue(undefined)
|
||||
|
||||
await auth.respond()
|
||||
|
||||
expect(auth.status).toBe(AuthStatus.DeniedSignature)
|
||||
expect(connection.send).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe("automatic authentication", () => {
|
||||
it("should auto-respond in implicit mode", () => {
|
||||
ctx.net.authMode = AuthMode.Implicit
|
||||
const respondSpy = vi.spyOn(auth, "respond")
|
||||
|
||||
connection.emit(ConnectionEvent.Receive, ["AUTH", "challenge123"])
|
||||
expect(respondSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should not auto-respond in explicit mode", () => {
|
||||
ctx.net.authMode = AuthMode.Explicit
|
||||
const respondSpy = vi.spyOn(auth, "respond")
|
||||
|
||||
connection.emit(ConnectionEvent.Receive, ["AUTH", "challenge123"])
|
||||
expect(respondSpy).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe("waitFor methods", () => {
|
||||
it("should wait for challenge", async () => {
|
||||
const waitPromise = auth.waitForChallenge()
|
||||
|
||||
setTimeout(() => {
|
||||
connection.emit(ConnectionEvent.Receive, ["AUTH", "challenge123"])
|
||||
}, 100)
|
||||
|
||||
vi.advanceTimersByTime(100)
|
||||
await waitPromise
|
||||
expect(auth.challenge).toBe("challenge123")
|
||||
})
|
||||
|
||||
it("should timeout waiting for challenge", async () => {
|
||||
const waitPromise = auth.waitForChallenge(50)
|
||||
|
||||
vi.advanceTimersByTime(100)
|
||||
await waitPromise
|
||||
expect(auth.challenge).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should wait for resolution", async () => {
|
||||
auth.challenge = "challenge123"
|
||||
auth.request = "request123"
|
||||
auth.status = AuthStatus.PendingResponse
|
||||
|
||||
const waitPromise = auth.waitForResolution()
|
||||
|
||||
setTimeout(() => {
|
||||
connection.emit(ConnectionEvent.Receive, ["OK", "request123", true, "success"])
|
||||
}, 100)
|
||||
|
||||
vi.advanceTimersByTime(100)
|
||||
await waitPromise
|
||||
expect(auth.status).toBe(AuthStatus.Ok)
|
||||
})
|
||||
|
||||
it("should timeout waiting for resolution", async () => {
|
||||
auth.status = AuthStatus.PendingResponse
|
||||
|
||||
const waitPromise = auth.waitForResolution(50)
|
||||
|
||||
vi.advanceTimersByTime(100)
|
||||
await waitPromise
|
||||
expect(auth.status).toBe(AuthStatus.PendingResponse)
|
||||
})
|
||||
})
|
||||
|
||||
describe("attempt()", () => {
|
||||
it("should complete full authentication flow", async () => {
|
||||
const signedEvent = {id: "event123" /* other event fields */}
|
||||
mockSignEvent.mockResolvedValue(signedEvent)
|
||||
|
||||
const attemptPromise = auth.attempt()
|
||||
|
||||
// Simulate socket opening and challenge received
|
||||
|
||||
setTimeout(() => {
|
||||
connection.emit(ConnectionEvent.Receive, ["AUTH", "challenge123"])
|
||||
}, 100)
|
||||
|
||||
await vi.advanceTimersByTimeAsync(100)
|
||||
|
||||
// Simulate successful authentication
|
||||
setTimeout(() => {
|
||||
connection.emit(ConnectionEvent.Receive, ["OK", "event123", true, "success"])
|
||||
}, 200)
|
||||
|
||||
await vi.advanceTimersByTimeAsync(200)
|
||||
|
||||
await attemptPromise
|
||||
|
||||
expect(auth.status).toBe(AuthStatus.Ok)
|
||||
})
|
||||
|
||||
it("should handle authentication failure", async () => {
|
||||
mockSignEvent.mockResolvedValue(undefined)
|
||||
|
||||
const attemptPromise = auth.attempt()
|
||||
|
||||
setTimeout(() => {
|
||||
connection.emit(ConnectionEvent.Receive, ["AUTH", "challenge123"])
|
||||
}, 100)
|
||||
|
||||
await vi.advanceTimersByTimeAsync(200)
|
||||
|
||||
await attemptPromise
|
||||
|
||||
expect(auth.status).toBe(AuthStatus.DeniedSignature)
|
||||
})
|
||||
|
||||
it("should timeout if no challenge received", async () => {
|
||||
const attemptPromise = auth.attempt(100)
|
||||
|
||||
// 2 loops (2 * 100ms) in the waitForChallenge before timeout
|
||||
// 1 loop in waitForResolution as it reach the condition immediately
|
||||
await vi.advanceTimersByTimeAsync(100)
|
||||
|
||||
await attemptPromise
|
||||
|
||||
expect(auth.status).toBe(AuthStatus.None)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,202 +0,0 @@
|
||||
import {ConnectionSender} from "../src/ConnectionSender"
|
||||
import {Connection} from "../src/Connection"
|
||||
import {Message, SocketStatus} from "../src/Socket"
|
||||
import {AuthStatus} from "../src/ConnectionAuth"
|
||||
import {AUTH_JOIN} from "@welshman/util"
|
||||
import {vi, describe, it, expect, beforeEach, afterEach} from "vitest"
|
||||
|
||||
describe("ConnectionSender", () => {
|
||||
let connection: Connection
|
||||
let sender: ConnectionSender
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
connection = new Connection("wss://test.relay/")
|
||||
connection.socket.send = vi.fn()
|
||||
connection.socket.open = vi.fn().mockResolvedValue(undefined)
|
||||
connection.socket.status = SocketStatus.Open
|
||||
connection.send = vi.fn().mockResolvedValue(undefined)
|
||||
|
||||
sender = connection.sender
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
describe("message deferral", () => {
|
||||
it("should not defer CLOSE messages", async () => {
|
||||
// First send a REQ message to set up the pending request
|
||||
const reqId = "subscription-id"
|
||||
connection.sender.push([
|
||||
"REQ",
|
||||
reqId,
|
||||
{
|
||||
/* filters */
|
||||
},
|
||||
] as Message)
|
||||
const message: Message = ["CLOSE", reqId]
|
||||
// there is a setTimeout in the worker, so we need to advance timers
|
||||
await vi.advanceTimersByTimeAsync(50)
|
||||
|
||||
connection.sender.push(message)
|
||||
// there is a setTimeout in the worker, so we need to advance timers
|
||||
await vi.advanceTimersByTimeAsync(150)
|
||||
|
||||
expect(connection.socket.send).toHaveBeenCalledWith(message)
|
||||
})
|
||||
|
||||
it("should defer messages when socket is not open", () => {
|
||||
connection.socket.status = SocketStatus.Closed
|
||||
const message: Message = [
|
||||
"EVENT",
|
||||
{
|
||||
/* event data */
|
||||
},
|
||||
]
|
||||
sender.push(message)
|
||||
expect(connection.socket.send).not.toHaveBeenCalled()
|
||||
expect(sender.worker.buffer).toContain(message)
|
||||
})
|
||||
|
||||
it("should not defer AUTH messages", () => {
|
||||
const message: Message = [
|
||||
"AUTH",
|
||||
{
|
||||
/* auth data */
|
||||
},
|
||||
]
|
||||
sender.push(message)
|
||||
// there is a setTimeout in the worker, so we need to advance timers
|
||||
vi.advanceTimersByTime(50)
|
||||
expect(connection.socket.send).toHaveBeenCalledWith(message)
|
||||
})
|
||||
|
||||
it("should not defer AUTH_JOIN event messages", () => {
|
||||
const message: Message = ["EVENT", {kind: AUTH_JOIN}]
|
||||
sender.push(message)
|
||||
// there is a setTimeout in the worker, so we need to advance timers
|
||||
vi.advanceTimersByTime(50)
|
||||
expect(connection.socket.send).toHaveBeenCalledWith(message)
|
||||
})
|
||||
|
||||
it("should defer messages when auth is pending", () => {
|
||||
connection.socket.status = SocketStatus.Open
|
||||
connection.auth.status = AuthStatus.PendingResponse
|
||||
const message: Message = [
|
||||
"EVENT",
|
||||
{
|
||||
/* event data */
|
||||
},
|
||||
]
|
||||
sender.push(message)
|
||||
vi.advanceTimersByTime(50)
|
||||
expect(connection.socket.send).not.toHaveBeenCalled()
|
||||
expect(sender.worker.buffer).toContain(message)
|
||||
})
|
||||
|
||||
it("should defer REQ messages when too many pending requests", () => {
|
||||
connection.socket.status = SocketStatus.Open
|
||||
connection.auth.status = AuthStatus.Ok
|
||||
// Set up 50 pending requests
|
||||
for (let i = 0; i < 50; i++) {
|
||||
connection.state.pendingRequests.set(`req${i}`, {
|
||||
filters: [],
|
||||
sent: Date.now(),
|
||||
})
|
||||
}
|
||||
|
||||
const message: Message = [
|
||||
"REQ",
|
||||
"newReq",
|
||||
{
|
||||
/* filter */
|
||||
},
|
||||
]
|
||||
sender.push(message)
|
||||
vi.advanceTimersByTime(50)
|
||||
expect(connection.socket.send).not.toHaveBeenCalled()
|
||||
expect(sender.worker.buffer).toContain(message)
|
||||
})
|
||||
})
|
||||
|
||||
describe("message handling", () => {
|
||||
it("should send messages when conditions are met", () => {
|
||||
connection.socket.status = SocketStatus.Open
|
||||
connection.auth.status = AuthStatus.Ok
|
||||
const message: Message = [
|
||||
"EVENT",
|
||||
{
|
||||
/* event data */
|
||||
},
|
||||
]
|
||||
sender.push(message)
|
||||
vi.advanceTimersByTime(50)
|
||||
expect(connection.socket.send).toHaveBeenCalledWith(message)
|
||||
})
|
||||
|
||||
it("should handle CLOSE messages for non-existent requests", () => {
|
||||
const message: Message = ["CLOSE", "non-existent-req"]
|
||||
sender.push(message)
|
||||
expect(connection.socket.send).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should remove pending REQ when handling CLOSE", () => {
|
||||
const reqId = "req123"
|
||||
const reqMessage: Message = [
|
||||
"REQ",
|
||||
reqId,
|
||||
{
|
||||
/* filter */
|
||||
},
|
||||
]
|
||||
sender.worker.buffer.push(reqMessage)
|
||||
|
||||
const closeMessage: Message = ["CLOSE", reqId]
|
||||
sender.push(closeMessage)
|
||||
|
||||
expect(sender.worker.buffer).not.toContain(reqMessage)
|
||||
})
|
||||
})
|
||||
|
||||
describe("worker behavior", () => {
|
||||
it("should process deferred messages when conditions become favorable", async () => {
|
||||
connection.socket.status = SocketStatus.Closed
|
||||
const message: Message = [
|
||||
"EVENT",
|
||||
{
|
||||
/* event data */
|
||||
},
|
||||
]
|
||||
sender.push(message)
|
||||
vi.advanceTimersByTime(50)
|
||||
expect(connection.socket.send).not.toHaveBeenCalled()
|
||||
|
||||
// Simulate socket opening and auth completing
|
||||
connection.socket.status = SocketStatus.Open
|
||||
connection.auth.status = AuthStatus.Ok
|
||||
|
||||
// Trigger worker processing
|
||||
sender.worker.resume()
|
||||
vi.advanceTimersByTime(50)
|
||||
expect(connection.socket.send).toHaveBeenCalledWith(message)
|
||||
})
|
||||
|
||||
it("should maintain message order", async () => {
|
||||
connection.socket.status = SocketStatus.Open
|
||||
connection.auth.status = AuthStatus.Ok
|
||||
|
||||
const messages: Message[] = [
|
||||
["EVENT", {id: "1"}],
|
||||
["EVENT", {id: "2"}],
|
||||
["EVENT", {id: "3"}],
|
||||
]
|
||||
|
||||
messages.forEach(msg => sender.push(msg))
|
||||
await vi.advanceTimersByTimeAsync(50)
|
||||
|
||||
const sendCalls = connection.socket.send.mock.calls
|
||||
expect(sendCalls.map(call => call[0])).toEqual(messages)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,201 +0,0 @@
|
||||
import {ConnectionState} from "../src/ConnectionState"
|
||||
import {Connection} from "../src/Connection"
|
||||
import {SocketStatus} from "../src/Socket"
|
||||
import {ConnectionEvent} from "../src/ConnectionEvent"
|
||||
import {AUTH_JOIN, SignedEvent} from "@welshman/util"
|
||||
import {vi, describe, it, expect, beforeEach} from "vitest"
|
||||
|
||||
describe("ConnectionState", () => {
|
||||
let connection: Connection
|
||||
let state: ConnectionState
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
connection = new Connection("wss://test.relay/")
|
||||
connection.socket.status = SocketStatus.Open
|
||||
connection.socket.send = vi.fn().mockResolvedValue(undefined)
|
||||
connection.socket.open = vi.fn().mockResolvedValue(undefined)
|
||||
connection.send = vi.fn().mockResolvedValue(undefined)
|
||||
state = connection.state
|
||||
})
|
||||
|
||||
describe("request tracking", () => {
|
||||
it("should track new REQ messages", async () => {
|
||||
const reqId = "req123"
|
||||
const filters = [{kinds: [1]}]
|
||||
|
||||
connection.sender.worker.push(["REQ", reqId, ...filters])
|
||||
await vi.advanceTimersByTimeAsync(50)
|
||||
|
||||
expect(state.pendingRequests.has(reqId)).toBe(true)
|
||||
expect(state.pendingRequests.get(reqId)).toEqual({
|
||||
filters,
|
||||
sent: Date.now(),
|
||||
eose: undefined,
|
||||
})
|
||||
})
|
||||
|
||||
it("should remove requests on CLOSE", async () => {
|
||||
const reqId = "req123"
|
||||
state.pendingRequests.set(reqId, {
|
||||
filters: [],
|
||||
sent: Date.now(),
|
||||
})
|
||||
|
||||
connection.socket.worker.push(["CLOSED", reqId])
|
||||
await vi.advanceTimersByTimeAsync(50)
|
||||
|
||||
expect(state.pendingRequests.has(reqId)).toBe(false)
|
||||
})
|
||||
|
||||
it("should mark requests as EOSE", async () => {
|
||||
const reqId = "req123"
|
||||
state.pendingRequests.set(reqId, {
|
||||
filters: [],
|
||||
sent: Date.now(),
|
||||
})
|
||||
|
||||
connection.socket.worker.push(["EOSE", reqId])
|
||||
await vi.advanceTimersByTimeAsync(50)
|
||||
|
||||
expect(state.pendingRequests.get(reqId)?.eose).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe("publish tracking", () => {
|
||||
it("should track EVENT messages", async () => {
|
||||
const event = {id: "event123", kind: 1}
|
||||
|
||||
connection.sender.worker.push(["EVENT", event])
|
||||
await vi.advanceTimersByTimeAsync(50)
|
||||
|
||||
expect(state.pendingPublishes.has(event.id)).toBeTruthy()
|
||||
expect(state.pendingPublishes.get(event.id)).toEqual({
|
||||
sent: Date.now(),
|
||||
event,
|
||||
})
|
||||
})
|
||||
|
||||
it("should remove publishes on successful OK", async () => {
|
||||
const eventId = "event123"
|
||||
state.pendingPublishes.set(eventId, {
|
||||
sent: Date.now(),
|
||||
event: {id: eventId, kind: 1} as SignedEvent,
|
||||
})
|
||||
|
||||
connection.socket.worker.push(["OK", eventId, true])
|
||||
await vi.advanceTimersByTimeAsync(50)
|
||||
|
||||
expect(state.pendingPublishes.has(eventId)).toBe(false)
|
||||
})
|
||||
|
||||
it("should re-enqueue events on auth challenge", async () => {
|
||||
const event = {id: "event123", kind: 1} as SignedEvent
|
||||
state.pendingPublishes.set(event.id, {
|
||||
sent: Date.now(),
|
||||
event,
|
||||
})
|
||||
|
||||
connection.socket.worker.push(["OK", event.id, false, "auth-required:challenge123"])
|
||||
await vi.advanceTimersByTimeAsync(50)
|
||||
|
||||
// Event should still be in pending publishes
|
||||
expect(state.pendingPublishes.has(event.id)).toBe(true)
|
||||
// And should have been re-sent
|
||||
expect(connection.send).toHaveBeenCalledWith(["EVENT", event])
|
||||
})
|
||||
|
||||
it("should not re-enqueue AUTH_JOIN events on auth challenge", async () => {
|
||||
const event = {id: "event123", kind: AUTH_JOIN} as SignedEvent
|
||||
state.pendingPublishes.set(event.id, {
|
||||
sent: Date.now(),
|
||||
event,
|
||||
})
|
||||
|
||||
connection.socket.worker.push(["OK", event.id, false, "auth-required:challenge123"])
|
||||
await vi.advanceTimersByTimeAsync(50)
|
||||
|
||||
// Event should be removed from pending publishes
|
||||
expect(state.pendingPublishes.has(event.id)).toBe(false)
|
||||
// And should not have been re-sent
|
||||
expect(connection.send).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe("notice handling", () => {
|
||||
it("should emit notices", async () => {
|
||||
const noticeSpy = vi.fn()
|
||||
connection.on(ConnectionEvent.Notice, noticeSpy)
|
||||
|
||||
connection.socket.worker.push(["NOTICE", "test notice"])
|
||||
await vi.advanceTimersByTimeAsync(50)
|
||||
|
||||
expect(noticeSpy).toHaveBeenCalledWith(connection, "test notice")
|
||||
})
|
||||
|
||||
it("should emit auth-required notice from CLOSED", async () => {
|
||||
const noticeSpy = vi.fn()
|
||||
connection.on(ConnectionEvent.Notice, noticeSpy)
|
||||
|
||||
connection.socket.worker.push(["CLOSED", "req123", "auth-required:challenge123"])
|
||||
await vi.advanceTimersByTimeAsync(50)
|
||||
|
||||
expect(noticeSpy).toHaveBeenCalledWith(connection, "auth-required:challenge123")
|
||||
})
|
||||
})
|
||||
|
||||
describe("reconnection behavior", () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
})
|
||||
|
||||
it("should re-enqueue pending requests on reconnection", async () => {
|
||||
const reqId = "req123"
|
||||
const filters = [{kinds: [1]}]
|
||||
state.pendingRequests.set(reqId, {
|
||||
filters,
|
||||
sent: Date.now(),
|
||||
})
|
||||
|
||||
// Simulate connection close and wait for reconnection delay
|
||||
connection.emit(ConnectionEvent.Close, connection)
|
||||
await vi.advanceTimersByTimeAsync(10_000)
|
||||
|
||||
expect(connection.send).toHaveBeenCalledWith(["REQ", reqId, ...filters])
|
||||
})
|
||||
|
||||
it("should re-enqueue pending publishes on reconnection", async () => {
|
||||
const event = {id: "event123", kind: 1} as SignedEvent
|
||||
state.pendingPublishes.set(event.id, {
|
||||
sent: Date.now(),
|
||||
event,
|
||||
})
|
||||
|
||||
// Simulate connection close and wait for reconnection delay
|
||||
connection.emit(ConnectionEvent.Close, connection)
|
||||
await vi.advanceTimersByTimeAsync(10_000)
|
||||
|
||||
expect(connection.send).toHaveBeenCalledWith(["EVENT", event])
|
||||
})
|
||||
|
||||
it("should trigger reconnection when there are pending items", async () => {
|
||||
const reqId = "req123"
|
||||
state.pendingRequests.set(reqId, {
|
||||
filters: [],
|
||||
sent: Date.now(),
|
||||
})
|
||||
|
||||
connection.emit(ConnectionEvent.Close, connection)
|
||||
await vi.advanceTimersByTimeAsync(10_000)
|
||||
|
||||
expect(connection.socket.open).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should not trigger reconnection when there are no pending items", async () => {
|
||||
connection.emit(ConnectionEvent.Close, connection)
|
||||
await vi.advanceTimersByTimeAsync(10_000)
|
||||
|
||||
expect(connection.socket.open).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,220 +0,0 @@
|
||||
import {ctx} from "@welshman/lib"
|
||||
import {AuthMode} from "@welshman/net"
|
||||
import {SignedEvent} from "@welshman/util"
|
||||
import {beforeEach, describe, expect, it, vi} from "vitest"
|
||||
import {Connection} from "../src/Connection"
|
||||
import {ConnectionEvent} from "../src/ConnectionEvent"
|
||||
import {ConnectionStats} from "../src/ConnectionStats"
|
||||
|
||||
describe("ConnectionStats", () => {
|
||||
let connection: Connection
|
||||
let stats: ConnectionStats
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
connection = new Connection("wss://test.relay/")
|
||||
stats = connection.stats
|
||||
ctx.net = {...ctx.net, authMode: AuthMode.Explicit}
|
||||
})
|
||||
|
||||
describe("connection events tracking", () => {
|
||||
it("should track socket open events", () => {
|
||||
const now = Date.now()
|
||||
connection.emit(ConnectionEvent.Open, connection)
|
||||
|
||||
expect(stats.openCount).toBe(1)
|
||||
expect(stats.lastOpen).toBeGreaterThanOrEqual(now)
|
||||
})
|
||||
|
||||
it("should track socket close events", () => {
|
||||
const now = Date.now()
|
||||
connection.emit(ConnectionEvent.Close, connection)
|
||||
|
||||
expect(stats.closeCount).toBe(1)
|
||||
expect(stats.lastClose).toBeGreaterThanOrEqual(now)
|
||||
})
|
||||
|
||||
it("should track socket error events", () => {
|
||||
const now = Date.now()
|
||||
connection.emit(ConnectionEvent.Error, connection)
|
||||
|
||||
expect(stats.errorCount).toBe(1)
|
||||
expect(stats.lastError).toBeGreaterThanOrEqual(now)
|
||||
})
|
||||
|
||||
it("should accumulate multiple events", () => {
|
||||
connection.emit(ConnectionEvent.Open, connection)
|
||||
connection.emit(ConnectionEvent.Close, connection)
|
||||
connection.emit(ConnectionEvent.Open, connection)
|
||||
connection.emit(ConnectionEvent.Error, connection)
|
||||
|
||||
expect(stats.openCount).toBe(2)
|
||||
expect(stats.closeCount).toBe(1)
|
||||
expect(stats.errorCount).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe("message tracking", () => {
|
||||
describe("outgoing messages", () => {
|
||||
it("should track REQ messages", () => {
|
||||
const now = Date.now()
|
||||
connection.emit(ConnectionEvent.Send, ["REQ", "id1"])
|
||||
|
||||
expect(stats.requestCount).toBe(1)
|
||||
expect(stats.lastRequest).toBeGreaterThanOrEqual(now)
|
||||
})
|
||||
|
||||
it("should track EVENT messages", () => {
|
||||
const now = Date.now()
|
||||
connection.emit(ConnectionEvent.Send, ["EVENT", {id: "123"}])
|
||||
|
||||
expect(stats.publishCount).toBe(1)
|
||||
expect(stats.lastPublish).toBeGreaterThanOrEqual(now)
|
||||
})
|
||||
})
|
||||
|
||||
describe("incoming messages", () => {
|
||||
it("should track received EVENT messages", () => {
|
||||
const now = Date.now()
|
||||
connection.emit(ConnectionEvent.Receive, ["EVENT", {id: "123"}])
|
||||
|
||||
expect(stats.eventCount).toBe(1)
|
||||
expect(stats.lastEvent).toBeGreaterThanOrEqual(now)
|
||||
})
|
||||
|
||||
it("should track AUTH messages", () => {
|
||||
const now = Date.now()
|
||||
connection.emit(ConnectionEvent.Receive, ["AUTH", "challenge"])
|
||||
|
||||
expect(stats.lastAuth).toBeGreaterThanOrEqual(now)
|
||||
})
|
||||
|
||||
it("should track NOTICE messages", () => {
|
||||
connection.emit(ConnectionEvent.Receive, ["NOTICE", "test"])
|
||||
expect(stats.noticeCount).toBe(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("publish tracking", () => {
|
||||
beforeEach(() => {
|
||||
// Setup a pending publish
|
||||
connection.state.pendingPublishes.set("123", {
|
||||
sent: Date.now() - 1000, // 1 second ago
|
||||
event: {id: "123"} as SignedEvent,
|
||||
})
|
||||
})
|
||||
|
||||
it("should track successful publishes", () => {
|
||||
connection.emit(ConnectionEvent.Receive, ["OK", "123", true])
|
||||
|
||||
expect(stats.publishSuccessCount).toBe(1)
|
||||
expect(stats.publishFailureCount).toBe(0)
|
||||
expect(stats.publishTimer).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it("should track failed publishes", () => {
|
||||
connection.emit(ConnectionEvent.Receive, ["OK", "123", false])
|
||||
|
||||
expect(stats.publishSuccessCount).toBe(0)
|
||||
expect(stats.publishFailureCount).toBe(1)
|
||||
expect(stats.publishTimer).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it("should accumulate publish timing", () => {
|
||||
const firstTimer = stats.publishTimer
|
||||
// First publish took 1000ms
|
||||
connection.emit(ConnectionEvent.Receive, ["OK", "123", true])
|
||||
|
||||
// Second publish took 2000ms
|
||||
connection.state.pendingPublishes.set("456", {
|
||||
sent: Date.now() - 2000,
|
||||
event: {id: "456"} as SignedEvent,
|
||||
})
|
||||
|
||||
connection.emit(ConnectionEvent.Receive, ["OK", "456", true])
|
||||
|
||||
expect(stats.publishTimer).toBe(firstTimer + 1000 + 2000)
|
||||
expect(stats.publishSuccessCount).toBe(2)
|
||||
})
|
||||
|
||||
it("should not increment publish timer for unknown publishes", () => {
|
||||
connection.emit(ConnectionEvent.Receive, ["OK", "unknown", true])
|
||||
|
||||
expect(stats.publishSuccessCount).toBe(1)
|
||||
expect(stats.publishFailureCount).toBe(0)
|
||||
expect(stats.publishTimer).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe("EOSE tracking", () => {
|
||||
beforeEach(() => {
|
||||
// Setup a pending request
|
||||
connection.state.pendingRequests.set("req1", {
|
||||
sent: Date.now() - 1000,
|
||||
filters: [],
|
||||
})
|
||||
})
|
||||
|
||||
it("should track first EOSE for a request", () => {
|
||||
connection.emit(ConnectionEvent.Receive, ["EOSE", "req1"])
|
||||
|
||||
expect(stats.eoseCount).toBe(1)
|
||||
expect(stats.eoseTimer).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it("should ignore subsequent EOSE for same request", () => {
|
||||
// Mark request as already EOSE'd
|
||||
connection.state.pendingRequests.set("req1", {
|
||||
sent: Date.now() - 1000,
|
||||
filters: [],
|
||||
eose: true,
|
||||
})
|
||||
|
||||
connection.emit(ConnectionEvent.Receive, ["EOSE", "req1"])
|
||||
|
||||
expect(stats.eoseCount).toBe(0)
|
||||
expect(stats.eoseTimer).toBe(0)
|
||||
})
|
||||
|
||||
it("should accumulate EOSE timing", () => {
|
||||
// First EOSE took 1000ms
|
||||
connection.emit(ConnectionEvent.Receive, ["EOSE", "req1"])
|
||||
const firstTimer = stats.eoseTimer
|
||||
|
||||
// Setup second request that takes 2000ms
|
||||
connection.state.pendingRequests.set("req2", {
|
||||
sent: Date.now() - 2000,
|
||||
filters: [],
|
||||
})
|
||||
connection.emit(ConnectionEvent.Receive, ["EOSE", "req2"])
|
||||
|
||||
expect(stats.eoseTimer).toBe(firstTimer + 2000)
|
||||
expect(stats.eoseCount).toBe(2)
|
||||
})
|
||||
})
|
||||
|
||||
describe("speed calculations", () => {
|
||||
it("should calculate request speed", () => {
|
||||
stats.eoseCount = 2
|
||||
stats.eoseTimer = 3000 // 3 seconds total for 2 requests
|
||||
|
||||
expect(stats.getRequestSpeed()).toBe(1500) // 1.5 seconds average
|
||||
})
|
||||
|
||||
it("should return 0 request speed when no EOSE received", () => {
|
||||
expect(stats.getRequestSpeed()).toBe(0)
|
||||
})
|
||||
|
||||
it("should calculate publish speed", () => {
|
||||
stats.publishSuccessCount = 2
|
||||
stats.publishTimer = 4000 // 4 seconds total for 2 publishes
|
||||
|
||||
expect(stats.getPublishSpeed()).toBe(2000) // 2 seconds average
|
||||
})
|
||||
|
||||
it("should return 0 publish speed when no successful publishes", () => {
|
||||
expect(stats.getPublishSpeed()).toBe(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,192 +0,0 @@
|
||||
import type {Filter, TrustedEvent} from "@welshman/util"
|
||||
import {hasValidSignature, isSignedEvent, LOCAL_RELAY_URL, matchFilters} from "@welshman/util"
|
||||
import {afterEach, beforeEach, describe, expect, it, vi} from "vitest"
|
||||
import {AuthMode} from "../src/ConnectionAuth"
|
||||
import {
|
||||
defaultOptimizeSubscriptions,
|
||||
eventValidationScores,
|
||||
getDefaultNetContext,
|
||||
isEventValid,
|
||||
} from "../src/Context"
|
||||
|
||||
// Mock utilities that are imported
|
||||
vi.mock(import("@welshman/util"), async importOriginal => ({
|
||||
...(await importOriginal()),
|
||||
isSignedEvent: vi.fn(),
|
||||
hasValidSignature: vi.fn(),
|
||||
matchFilters: vi.fn(),
|
||||
LOCAL_RELAY_URL: "local",
|
||||
}))
|
||||
|
||||
describe("Context", () => {
|
||||
describe("getDefaultNetContext", () => {
|
||||
it("should return default context with expected properties", () => {
|
||||
const context = getDefaultNetContext()
|
||||
|
||||
expect(context).toEqual(
|
||||
expect.objectContaining({
|
||||
authMode: AuthMode.Implicit,
|
||||
onEvent: expect.any(Function),
|
||||
signEvent: expect.any(Function),
|
||||
isDeleted: expect.any(Function),
|
||||
isValid: expect.any(Function),
|
||||
getExecutor: expect.any(Function),
|
||||
matchFilters: expect.any(Function),
|
||||
optimizeSubscriptions: expect.any(Function),
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it("should merge overrides with defaults", () => {
|
||||
const customOnEvent = vi.fn()
|
||||
const context = getDefaultNetContext({onEvent: customOnEvent})
|
||||
|
||||
expect(context.onEvent).toBe(customOnEvent)
|
||||
expect(context.authMode).toBe(AuthMode.Implicit) // default value preserved
|
||||
})
|
||||
})
|
||||
|
||||
describe("defaultOptimizeSubscriptions", () => {
|
||||
it("should group subscriptions by relay", () => {
|
||||
const subs = [
|
||||
{
|
||||
request: {
|
||||
relays: ["relay1", "relay2"],
|
||||
filters: [{kinds: [1]}],
|
||||
},
|
||||
},
|
||||
{
|
||||
request: {
|
||||
relays: ["relay1"],
|
||||
filters: [{kinds: [2]}],
|
||||
},
|
||||
},
|
||||
] as any
|
||||
|
||||
const result = defaultOptimizeSubscriptions(subs)
|
||||
// should unionize filters for requests with the same relay
|
||||
expect(result).toEqual([
|
||||
{
|
||||
relays: ["relay1"],
|
||||
filters: expect.arrayContaining([{kinds: [1, 2]}]),
|
||||
},
|
||||
{
|
||||
relays: ["relay2"],
|
||||
filters: [{kinds: [1]}],
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it("should deduplicate relays", () => {
|
||||
const subs = [
|
||||
{
|
||||
request: {
|
||||
relays: ["relay1", "relay1"],
|
||||
filters: [{kinds: [1]}],
|
||||
},
|
||||
},
|
||||
] as any
|
||||
|
||||
const result = defaultOptimizeSubscriptions(subs)
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].relays).toEqual(["relay1"])
|
||||
})
|
||||
})
|
||||
|
||||
describe("isEventValid", () => {
|
||||
const mockEvent = {id: "123"} as TrustedEvent
|
||||
beforeEach(() => {
|
||||
eventValidationScores.clear()
|
||||
// vi.mocked(isSignedEvent)
|
||||
// vi.mocked(hasValidSignature)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it("should always return true for LOCAL_RELAY_URL", () => {
|
||||
expect(isEventValid(LOCAL_RELAY_URL, mockEvent)).toBe(true)
|
||||
})
|
||||
|
||||
it("should validate signature for non-local events", () => {
|
||||
vi.mocked(isSignedEvent).mockReturnValue(true)
|
||||
vi.mocked(hasValidSignature).mockReturnValue(true)
|
||||
|
||||
const result = isEventValid("relay1", mockEvent)
|
||||
|
||||
expect(isSignedEvent).toHaveBeenCalledWith(mockEvent)
|
||||
expect(hasValidSignature).toHaveBeenCalledWith(mockEvent)
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
|
||||
it("should update validation score on successful validation", () => {
|
||||
vi.mocked(isSignedEvent).mockReturnValue(true)
|
||||
vi.mocked(hasValidSignature).mockReturnValue(true)
|
||||
|
||||
isEventValid("relay1", mockEvent)
|
||||
|
||||
expect(eventValidationScores.get("relay1")).toBe(1)
|
||||
})
|
||||
|
||||
it("should reset validation score on failed validation", () => {
|
||||
// Set initial score
|
||||
eventValidationScores.set("relay1", 10)
|
||||
|
||||
vi.mocked(isSignedEvent).mockReturnValue(false)
|
||||
vi.mocked(hasValidSignature).mockReturnValue(true)
|
||||
|
||||
isEventValid("relay1", mockEvent)
|
||||
|
||||
expect(eventValidationScores.get("relay1")).toBe(0)
|
||||
})
|
||||
|
||||
it("should skip validation when score is high enough", () => {
|
||||
eventValidationScores.set("relay1", 1000)
|
||||
|
||||
const result = isEventValid("relay1", mockEvent)
|
||||
|
||||
expect(isSignedEvent).not.toHaveBeenCalled()
|
||||
expect(hasValidSignature).not.toHaveBeenCalled()
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
|
||||
it("should maintain minimum validation rate", () => {
|
||||
eventValidationScores.set("relay1", 800)
|
||||
vi.spyOn(Math, "random").mockReturnValue(1000) // ensure randomInt returns
|
||||
vi.mocked(isSignedEvent).mockReturnValue(true)
|
||||
vi.mocked(hasValidSignature).mockReturnValue(true)
|
||||
|
||||
isEventValid("relay1", mockEvent)
|
||||
|
||||
expect(eventValidationScores.get("relay1")).toBe(801)
|
||||
})
|
||||
})
|
||||
|
||||
describe("default functions behavior", () => {
|
||||
const context = getDefaultNetContext()
|
||||
|
||||
it("default onEvent should not throw", () => {
|
||||
expect(() => context.onEvent("relay1", {} as TrustedEvent)).not.toThrow()
|
||||
})
|
||||
|
||||
it("default signEvent should return undefined", async () => {
|
||||
const result = await context.signEvent({} as any)
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
|
||||
it("default isDeleted should return false", () => {
|
||||
expect(context.isDeleted("relay1", {} as TrustedEvent)).toBe(false)
|
||||
})
|
||||
|
||||
it("default matchFilters should use util matchFilters", () => {
|
||||
const filters: Filter[] = []
|
||||
const event = {} as TrustedEvent
|
||||
|
||||
context.matchFilters("relay1", filters, event)
|
||||
|
||||
expect(vi.mocked(matchFilters)).toHaveBeenCalledWith(filters, event)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,256 +0,0 @@
|
||||
import {ctx} from "@welshman/lib"
|
||||
import type {Filter, SignedEvent, TrustedEvent} from "@welshman/util"
|
||||
import {afterEach, beforeEach, describe, expect, it, vi} from "vitest"
|
||||
import {Executor} from "../src/Executor"
|
||||
import {Negentropy} from "../src/Negentropy"
|
||||
|
||||
// Mock Negentropy
|
||||
vi.mock("../src/Negentropy.js", () => ({
|
||||
Negentropy: vi.fn().mockImplementation(() => ({
|
||||
reconcile: vi.fn().mockResolvedValue(["newMsg", ["id1"], ["id2"]]),
|
||||
initiate: vi.fn().mockResolvedValue("initialMsg"),
|
||||
})),
|
||||
NegentropyStorageVector: vi.fn().mockImplementation(() => ({
|
||||
insert: vi.fn(),
|
||||
seal: vi.fn(),
|
||||
})),
|
||||
}))
|
||||
|
||||
describe("Executor", () => {
|
||||
let mockTarget: any
|
||||
// let mockNegentropy: any
|
||||
let executor: Executor
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
// Setup mock target
|
||||
mockTarget = {
|
||||
connections: [],
|
||||
send: vi.fn().mockResolvedValue(undefined),
|
||||
on: vi.fn(),
|
||||
off: vi.fn(),
|
||||
cleanup: vi.fn(),
|
||||
}
|
||||
|
||||
// Setup mock context
|
||||
ctx.net = {
|
||||
...ctx.net,
|
||||
onEvent: vi.fn(),
|
||||
}
|
||||
|
||||
executor = new Executor(mockTarget)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe("subscribe", () => {
|
||||
const filters: Filter[] = [{kinds: [1]}]
|
||||
|
||||
it("should setup subscription correctly", () => {
|
||||
const onEvent = vi.fn()
|
||||
const onEose = vi.fn()
|
||||
|
||||
executor.subscribe(filters, {onEvent, onEose})
|
||||
|
||||
expect(mockTarget.on).toHaveBeenCalledWith("EVENT", expect.any(Function))
|
||||
expect(mockTarget.on).toHaveBeenCalledWith("EOSE", expect.any(Function))
|
||||
expect(mockTarget.send).toHaveBeenCalledWith("REQ", expect.any(String), ...filters)
|
||||
})
|
||||
|
||||
it("should handle events for matching subscription ID", () => {
|
||||
const onEvent = vi.fn()
|
||||
executor.subscribe(filters, {onEvent})
|
||||
|
||||
// Get the event listener that was registered
|
||||
const eventListener = mockTarget.on.mock.calls.find(call => call[0] === "EVENT")[1]
|
||||
const event = {id: "123"} as TrustedEvent
|
||||
|
||||
// Simulate event with matching subId (extract it from the REQ call)
|
||||
const subId = mockTarget.send.mock.calls[0][1]
|
||||
eventListener("relay1", subId, event)
|
||||
|
||||
expect(ctx.net.onEvent).toHaveBeenCalledWith("relay1", event)
|
||||
expect(onEvent).toHaveBeenCalledWith("relay1", event)
|
||||
})
|
||||
|
||||
it("should handle EOSE for matching subscription ID", () => {
|
||||
const onEose = vi.fn()
|
||||
executor.subscribe(filters, {onEose})
|
||||
|
||||
const eoseListener = mockTarget.on.mock.calls.find(call => call[0] === "EOSE")[1]
|
||||
const subId = mockTarget.send.mock.calls[0][1]
|
||||
|
||||
eoseListener("relay1", subId)
|
||||
|
||||
expect(onEose).toHaveBeenCalledWith("relay1")
|
||||
})
|
||||
|
||||
it("should cleanup on unsubscribe", () => {
|
||||
const sub = executor.subscribe(filters)
|
||||
const subId = mockTarget.send.mock.calls[0][1]
|
||||
|
||||
sub.unsubscribe()
|
||||
|
||||
expect(mockTarget.send).toHaveBeenLastCalledWith("CLOSE", subId)
|
||||
expect(mockTarget.off).toHaveBeenCalledTimes(2) // EVENT and EOSE listeners
|
||||
})
|
||||
|
||||
it("should not send CLOSE multiple times", () => {
|
||||
const sub = executor.subscribe(filters)
|
||||
sub.unsubscribe()
|
||||
const sendCallCount = mockTarget.send.mock.calls.length
|
||||
|
||||
sub.unsubscribe()
|
||||
|
||||
expect(mockTarget.send.mock.calls.length).toBe(sendCallCount)
|
||||
})
|
||||
})
|
||||
|
||||
describe("publish", () => {
|
||||
const event: SignedEvent = {
|
||||
id: "event123",
|
||||
kind: 1,
|
||||
content: "",
|
||||
tags: [],
|
||||
created_at: 0,
|
||||
pubkey: "",
|
||||
sig: "",
|
||||
}
|
||||
|
||||
it("should setup publish correctly", () => {
|
||||
const onOk = vi.fn()
|
||||
const onError = vi.fn()
|
||||
|
||||
executor.publish(event, {onOk, onError})
|
||||
|
||||
expect(mockTarget.on).toHaveBeenCalledWith("OK", expect.any(Function))
|
||||
expect(mockTarget.on).toHaveBeenCalledWith("ERROR", expect.any(Function))
|
||||
expect(mockTarget.send).toHaveBeenCalledWith("EVENT", event)
|
||||
})
|
||||
|
||||
it("should handle successful publish", () => {
|
||||
const onOk = vi.fn()
|
||||
executor.publish(event, {onOk})
|
||||
|
||||
const okListener = mockTarget.on.mock.calls.find(call => call[0] === "OK")[1]
|
||||
okListener("relay1", event.id, true, "success")
|
||||
|
||||
expect(ctx.net.onEvent).toHaveBeenCalledWith("relay1", event)
|
||||
expect(onOk).toHaveBeenCalledWith("relay1", event.id, true, "success")
|
||||
})
|
||||
|
||||
it("should handle failed publish", () => {
|
||||
const onOk = vi.fn()
|
||||
executor.publish(event, {onOk})
|
||||
|
||||
const okListener = mockTarget.on.mock.calls.find(call => call[0] === "OK")[1]
|
||||
okListener("relay1", event.id, false, "failed")
|
||||
|
||||
expect(ctx.net.onEvent).not.toHaveBeenCalled()
|
||||
expect(onOk).toHaveBeenCalledWith("relay1", event.id, false, "failed")
|
||||
})
|
||||
|
||||
it("should handle publish errors", () => {
|
||||
const onError = vi.fn()
|
||||
executor.publish(event, {onError})
|
||||
|
||||
const errorListener = mockTarget.on.mock.calls.find(call => call[0] === "ERROR")[1]
|
||||
errorListener("relay1", event.id, "error message")
|
||||
|
||||
expect(onError).toHaveBeenCalledWith("relay1", event.id, "error message")
|
||||
})
|
||||
|
||||
it("should cleanup on unsubscribe", () => {
|
||||
const pub = executor.publish(event)
|
||||
pub.unsubscribe()
|
||||
|
||||
expect(mockTarget.off).toHaveBeenCalledTimes(2) // OK and ERROR listeners
|
||||
})
|
||||
})
|
||||
|
||||
describe("diff", () => {
|
||||
const filter: Filter = {kinds: [1]}
|
||||
const events: TrustedEvent[] = [
|
||||
{id: "event1", created_at: 1000} as TrustedEvent,
|
||||
{id: "event2", created_at: 2000} as TrustedEvent,
|
||||
]
|
||||
|
||||
it("should setup diff correctly", async () => {
|
||||
const onMessage = vi.fn()
|
||||
const onError = vi.fn()
|
||||
const onClose = vi.fn()
|
||||
|
||||
executor.diff(filter, events, {onMessage, onError, onClose})
|
||||
|
||||
expect(mockTarget.on).toHaveBeenCalledWith("NEG-MSG", expect.any(Function))
|
||||
expect(mockTarget.on).toHaveBeenCalledWith("NEG-ERR", expect.any(Function))
|
||||
// Wait for initiate promise
|
||||
await vi.runAllTimersAsync()
|
||||
expect(mockTarget.send).toHaveBeenCalledWith(
|
||||
"NEG-OPEN",
|
||||
expect.any(String),
|
||||
filter,
|
||||
"initialMsg",
|
||||
)
|
||||
})
|
||||
|
||||
it("should handle diff messages", async () => {
|
||||
const onMessage = vi.fn()
|
||||
executor.diff(filter, events, {onMessage})
|
||||
|
||||
const msgListener = mockTarget.on.mock.calls.find(call => call[0] === "NEG-MSG")[1]
|
||||
// wait for initiate promise
|
||||
await vi.advanceTimersToNextTimerAsync()
|
||||
|
||||
await msgListener("relay1", mockTarget.send.mock.calls[0][1], "msg")
|
||||
|
||||
expect(onMessage).toHaveBeenCalledWith("relay1", {
|
||||
have: ["id1"],
|
||||
need: ["id2"],
|
||||
})
|
||||
})
|
||||
|
||||
it("should handle diff errors", async () => {
|
||||
const onError = vi.fn()
|
||||
executor.diff(filter, events, {onError})
|
||||
|
||||
const errListener = mockTarget.on.mock.calls.find(call => call[0] === "NEG-ERR")[1]
|
||||
// wait for initiate promise
|
||||
await vi.advanceTimersToNextTimerAsync()
|
||||
|
||||
errListener("relay1", mockTarget.send.mock.calls[0][1], "error")
|
||||
|
||||
expect(onError).toHaveBeenCalledWith("relay1", "error")
|
||||
})
|
||||
|
||||
it("should close diff when reconciliation completes", async () => {
|
||||
const onClose = vi.fn()
|
||||
executor.diff(filter, events, {onClose})
|
||||
|
||||
const msgListener = mockTarget.on.mock.calls.find(call => call[0] === "NEG-MSG")[1]
|
||||
// wait for initiate promise
|
||||
await vi.advanceTimersToNextTimerAsync()
|
||||
|
||||
// Get the mock instance's reconcile function from the last Negentropy constructor call
|
||||
const mockReconcile = vi.mocked(Negentropy).mock.results[0].value.reconcile
|
||||
mockReconcile.mockResolvedValueOnce([null, [], []])
|
||||
const reqId = mockTarget.send.mock.calls[0][1]
|
||||
|
||||
await msgListener("relay1", reqId, "msg")
|
||||
|
||||
expect(mockTarget.send).toHaveBeenCalledWith("NEG-CLOSE", reqId)
|
||||
expect(onClose).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should cleanup on unsubscribe", () => {
|
||||
const diff = executor.diff(filter, events)
|
||||
diff.unsubscribe()
|
||||
|
||||
expect(mockTarget.send).toHaveBeenCalledWith("NEG-CLOSE", expect.any(String))
|
||||
expect(mockTarget.off).toHaveBeenCalledTimes(2) // NEG-MSG and NEG-ERR listeners
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,125 +1,130 @@
|
||||
import {Pool} from "../src/Pool"
|
||||
import {Connection} from "../src/Connection"
|
||||
import {vi, describe, it, expect, beforeEach} from "vitest"
|
||||
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"
|
||||
|
||||
// Mock Connection class
|
||||
vi.mock("../src/Connection", () => ({
|
||||
Connection: vi.fn().mockImplementation(url => ({
|
||||
url,
|
||||
cleanup: vi.fn(),
|
||||
})),
|
||||
}))
|
||||
vi.mock('isomorphic-ws', () => {
|
||||
const WebSocket = vi.fn(function () {
|
||||
setTimeout(() => this.onopen())
|
||||
})
|
||||
|
||||
WebSocket.prototype.send = vi.fn()
|
||||
|
||||
WebSocket.prototype.close = vi.fn(function () {
|
||||
this.onclose()
|
||||
})
|
||||
|
||||
return { default: WebSocket }
|
||||
})
|
||||
|
||||
describe("Pool", () => {
|
||||
let pool: Pool
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
pool = new Pool()
|
||||
})
|
||||
|
||||
describe("initialization", () => {
|
||||
it("should initialize with empty data map", () => {
|
||||
expect(pool.data.size).toBe(0)
|
||||
})
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe("has", () => {
|
||||
it("should return false for non-existent connection", () => {
|
||||
it("should return false for non-existent socket", () => {
|
||||
expect(pool.has("wss://test.relay")).toBe(false)
|
||||
})
|
||||
|
||||
it("should return true for existing connection", () => {
|
||||
pool.get("wss://test.relay")
|
||||
it("should return true for existing socket, normalizing the url", () => {
|
||||
pool.get("wss://test.relay/")
|
||||
expect(pool.has("wss://test.relay")).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe("get", () => {
|
||||
it("should create new connection if none exists", () => {
|
||||
const connection = pool.get("wss://test.relay")
|
||||
it("should create new socket if none exists, normalizing the relay url", () => {
|
||||
const socket = pool.get("wss://test.relay")
|
||||
|
||||
expect(Connection).toHaveBeenCalledWith("wss://test.relay")
|
||||
expect(pool.data.get("wss://test.relay")).toBe(connection)
|
||||
expect(socket.url).toEqual("wss://test.relay/")
|
||||
})
|
||||
|
||||
it("should emit init event for new connections", () => {
|
||||
const initSpy = vi.fn()
|
||||
pool.on("init", initSpy)
|
||||
it("should return existing socket if it exists", () => {
|
||||
const firstSocket = pool.get("wss://test.relay")
|
||||
const secondSocket = pool.get("wss://test.relay")
|
||||
|
||||
const connection = pool.get("wss://test.relay")
|
||||
|
||||
expect(initSpy).toHaveBeenCalledWith(connection)
|
||||
expect(firstSocket).toBe(secondSocket)
|
||||
})
|
||||
})
|
||||
|
||||
it("should return existing connection if it exists", () => {
|
||||
const firstConnection = pool.get("wss://test.relay")
|
||||
const secondConnection = pool.get("wss://test.relay")
|
||||
describe("subscribe", () => {
|
||||
it("should notify subscribers of new sockets", () => {
|
||||
const sub1 = vi.fn()
|
||||
const sub2 = vi.fn()
|
||||
|
||||
expect(Connection).toHaveBeenCalledTimes(1)
|
||||
expect(firstConnection).toBe(secondConnection)
|
||||
})
|
||||
|
||||
it("should not emit init event for existing connections", () => {
|
||||
const initSpy = vi.fn()
|
||||
pool.subscribe(sub1)
|
||||
pool.subscribe(sub2)
|
||||
pool.get("wss://test.relay")
|
||||
|
||||
pool.on("init", initSpy)
|
||||
expect(sub1).toHaveBeenCalledTimes(1)
|
||||
expect(sub2).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("should not notify subscribers for existing sockets", () => {
|
||||
pool.get("wss://test.relay")
|
||||
|
||||
expect(initSpy).not.toHaveBeenCalled()
|
||||
const sub = vi.fn()
|
||||
pool.subscribe(sub)
|
||||
pool.get("wss://test.relay")
|
||||
|
||||
expect(sub).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should add subscription", () => {
|
||||
const sub = vi.fn()
|
||||
pool.subscribe(sub)
|
||||
expect(pool._subs).toContain(sub)
|
||||
})
|
||||
|
||||
it("should return unsubscribe function", () => {
|
||||
const sub = vi.fn()
|
||||
const unsubscribe = pool.subscribe(sub)
|
||||
|
||||
unsubscribe()
|
||||
|
||||
expect(pool._subs).not.toContain(sub)
|
||||
})
|
||||
})
|
||||
|
||||
describe("remove", () => {
|
||||
it("should remove existing connection", () => {
|
||||
const connection = pool.get("wss://test.relay")
|
||||
pool.remove("wss://test.relay")
|
||||
it("should remove and cleanup existing socket", () => {
|
||||
const mockSocket = { url: "wss://test.relay", cleanup: vi.fn() }
|
||||
|
||||
expect(pool.has("wss://test.relay")).toBe(false)
|
||||
expect(connection.cleanup).toHaveBeenCalled()
|
||||
pool._data.set(mockSocket.url, mockSocket)
|
||||
pool.remove(mockSocket.url)
|
||||
|
||||
expect(mockSocket.cleanup).toHaveBeenCalled()
|
||||
expect(pool._data.has(mockSocket.url)).toBe(false)
|
||||
})
|
||||
|
||||
it("should do nothing for non-existent connection", () => {
|
||||
it("should do nothing for non-existent socket", () => {
|
||||
pool.remove("wss://test.relay")
|
||||
expect(pool.has("wss://test.relay")).toBe(false)
|
||||
})
|
||||
|
||||
it("should cleanup connection before removal", () => {
|
||||
const connection = pool.get("wss://test.relay")
|
||||
pool.remove("wss://test.relay")
|
||||
|
||||
const spy = vi.spyOn(pool.data, "delete")
|
||||
|
||||
expect(connection.cleanup).toHaveBeenCalled()
|
||||
expect(pool._data.has("wss://test.relay")).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("clear", () => {
|
||||
it("should remove all connections", () => {
|
||||
const urls = ["wss://test1.relay", "wss://test2.relay", "wss://test3.relay"]
|
||||
it("should remove all sockets", () => {
|
||||
const urls = ["wss://test1.relay", "wss://test2.relay"]
|
||||
const mockSockets = urls.map(url => ({ url, cleanup: vi.fn() }))
|
||||
|
||||
// Create multiple connections
|
||||
urls.forEach(url => pool.get(url))
|
||||
expect(pool.data.size).toBe(3)
|
||||
for (const mockSocket of mockSockets) {
|
||||
pool._data.set(mockSocket.url, mockSocket)
|
||||
}
|
||||
|
||||
pool.clear()
|
||||
expect(pool.data.size).toBe(0)
|
||||
})
|
||||
|
||||
it("should cleanup all connections", () => {
|
||||
const urls = ["wss://test1.relay", "wss://test2.relay", "wss://test3.relay"]
|
||||
|
||||
const connections = urls.map(url => pool.get(url))
|
||||
pool.clear()
|
||||
|
||||
connections.forEach(connection => {
|
||||
expect(connection.cleanup).toHaveBeenCalled()
|
||||
expect(pool._data.size).toBe(0)
|
||||
mockSockets.forEach(socket => {
|
||||
expect(socket.cleanup).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
it("should do nothing on empty pool", () => {
|
||||
expect(() => pool.clear()).not.toThrow()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,184 +1,228 @@
|
||||
import {ctx} from "@welshman/lib"
|
||||
import type {SignedEvent} from "@welshman/util"
|
||||
import {afterEach, beforeEach, describe, expect, it, vi} from "vitest"
|
||||
import {makePublish, publish, PublishStatus} from "../src/Publish"
|
||||
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest"
|
||||
import { EventEmitter } from "events"
|
||||
import { Unicast, Multicast, PublishEventType, PublishStatus, unicast, multicast } from "../src/publish"
|
||||
import { AbstractAdapter, AdapterEventType } from "../src/adapter"
|
||||
import { ClientMessageType, RelayMessage } from "../src/message"
|
||||
import { SignedEvent, makeEvent } from "@welshman/util"
|
||||
import { Nip01Signer } from '@welshman/signer'
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock("@welshman/lib", async importOriginal => {
|
||||
return {
|
||||
...(await importOriginal()),
|
||||
randomId: () => "test-id",
|
||||
now: () => 1000,
|
||||
defer: () => ({
|
||||
resolve: vi.fn(),
|
||||
reject: vi.fn(),
|
||||
promise: Promise.resolve(),
|
||||
}),
|
||||
class MockAdapter extends AbstractAdapter {
|
||||
constructor(readonly url: string, readonly send) {
|
||||
super()
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock("@welshman/util", () => ({
|
||||
asSignedEvent: vi.fn(event => event),
|
||||
}))
|
||||
get sockets() {
|
||||
return []
|
||||
}
|
||||
|
||||
describe("Publish", () => {
|
||||
let mockExecutor: any
|
||||
let mockExecutorSub: any
|
||||
get urls() {
|
||||
return [this.url]
|
||||
}
|
||||
|
||||
receive = (message: RelayMessage) => {
|
||||
this.emit(AdapterEventType.Receive, message, this.url)
|
||||
}
|
||||
}
|
||||
|
||||
describe("Unicast", () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
|
||||
mockExecutorSub = {
|
||||
unsubscribe: vi.fn(),
|
||||
}
|
||||
|
||||
mockExecutor = {
|
||||
publish: vi.fn().mockReturnValue(mockExecutorSub),
|
||||
target: {
|
||||
cleanup: vi.fn(),
|
||||
},
|
||||
}
|
||||
|
||||
ctx.net = {
|
||||
...ctx.net,
|
||||
getExecutor: vi.fn().mockReturnValue(mockExecutor),
|
||||
}
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
describe("makePublish", () => {
|
||||
it("should create publish object with correct properties", () => {
|
||||
const request = {
|
||||
event: {id: "event123"} as SignedEvent,
|
||||
relays: ["relay1"],
|
||||
}
|
||||
it("success works", async () => {
|
||||
const sendSpy = vi.fn()
|
||||
const adapter = new MockAdapter('1', sendSpy)
|
||||
const signer = Nip01Signer.ephemeral()
|
||||
const event = await signer.sign(makeEvent(1))
|
||||
|
||||
const pub = makePublish(request)
|
||||
|
||||
expect(pub).toEqual({
|
||||
id: "test-id",
|
||||
created_at: 1000,
|
||||
request,
|
||||
emitter: expect.any(Object),
|
||||
result: expect.any(Object),
|
||||
status: expect.any(Map),
|
||||
})
|
||||
const pub = unicast({
|
||||
relay: '1',
|
||||
context: {getAdapter: () => adapter},
|
||||
event,
|
||||
})
|
||||
|
||||
const successSpy = vi.fn()
|
||||
const failureSpy = vi.fn()
|
||||
const completeSpy = vi.fn()
|
||||
|
||||
pub.on(PublishEventType.Success, successSpy)
|
||||
pub.on(PublishEventType.Failure, failureSpy)
|
||||
pub.on(PublishEventType.Complete, completeSpy)
|
||||
|
||||
await vi.advanceTimersByTimeAsync(200)
|
||||
|
||||
expect(sendSpy).toHaveBeenCalledWith([ClientMessageType.Event, event])
|
||||
|
||||
adapter.receive(["OK", event.id, true, "hi"])
|
||||
|
||||
await vi.runAllTimers()
|
||||
|
||||
expect(successSpy).toHaveBeenCalledWith(event.id, "hi")
|
||||
expect(failureSpy).not.toHaveBeenCalled()
|
||||
expect(completeSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
describe("publish", () => {
|
||||
const event = {id: "event123"} as SignedEvent
|
||||
const relays = ["relay1", "relay2"]
|
||||
it("failure works", async () => {
|
||||
const sendSpy = vi.fn()
|
||||
const adapter = new MockAdapter('1', sendSpy)
|
||||
const signer = Nip01Signer.ephemeral()
|
||||
const event = await signer.sign(makeEvent(1))
|
||||
|
||||
it("should initialize publish with pending status", async () => {
|
||||
const pub = publish({event, relays})
|
||||
|
||||
await vi.advanceTimersToNextTimerAsync()
|
||||
|
||||
relays.forEach(relay => {
|
||||
expect(pub.status.get(relay)).toBe(PublishStatus.Pending)
|
||||
})
|
||||
const pub = unicast({
|
||||
relay: '1',
|
||||
context: {getAdapter: () => adapter},
|
||||
event,
|
||||
})
|
||||
|
||||
it("should delegate to executor with correct parameters", () => {
|
||||
publish({event, relays})
|
||||
const successSpy = vi.fn()
|
||||
const failureSpy = vi.fn()
|
||||
const completeSpy = vi.fn()
|
||||
|
||||
expect(ctx.net.getExecutor).toHaveBeenCalledWith(relays)
|
||||
expect(mockExecutor.publish).toHaveBeenCalledWith(
|
||||
event,
|
||||
expect.objectContaining({
|
||||
verb: "EVENT",
|
||||
onOk: expect.any(Function),
|
||||
onError: expect.any(Function),
|
||||
}),
|
||||
)
|
||||
pub.on(PublishEventType.Success, successSpy)
|
||||
pub.on(PublishEventType.Failure, failureSpy)
|
||||
pub.on(PublishEventType.Complete, completeSpy)
|
||||
|
||||
await vi.advanceTimersByTimeAsync(200)
|
||||
|
||||
expect(sendSpy).toHaveBeenCalledWith([ClientMessageType.Event, event])
|
||||
|
||||
adapter.receive(["OK", event.id, false, "hi"])
|
||||
|
||||
await vi.runAllTimers()
|
||||
|
||||
expect(successSpy).not.toHaveBeenCalled()
|
||||
expect(failureSpy).toHaveBeenCalledWith(event.id, "hi")
|
||||
expect(completeSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("timeout works", async () => {
|
||||
const sendSpy = vi.fn()
|
||||
const adapter = new MockAdapter('1', sendSpy)
|
||||
const signer = Nip01Signer.ephemeral()
|
||||
const event = await signer.sign(makeEvent(1))
|
||||
|
||||
const pub = unicast({
|
||||
relay: '1',
|
||||
context: {getAdapter: () => adapter},
|
||||
event,
|
||||
})
|
||||
|
||||
it("should handle successful publish", async () => {
|
||||
const pub = publish({event, relays})
|
||||
await vi.runAllTimersAsync()
|
||||
const successSpy = vi.fn()
|
||||
const failureSpy = vi.fn()
|
||||
const completeSpy = vi.fn()
|
||||
const timeoutSpy = vi.fn()
|
||||
|
||||
const onOk = mockExecutor.publish.mock.calls[0][1].onOk
|
||||
onOk("relay1", event.id, true, "success")
|
||||
pub.on(PublishEventType.Success, successSpy)
|
||||
pub.on(PublishEventType.Failure, failureSpy)
|
||||
pub.on(PublishEventType.Complete, completeSpy)
|
||||
pub.on(PublishEventType.Timeout, timeoutSpy)
|
||||
|
||||
expect(pub.status.get("relay1")).toBe(PublishStatus.Success)
|
||||
await vi.runAllTimers(200)
|
||||
|
||||
expect(sendSpy).toHaveBeenCalledWith([ClientMessageType.Event, event])
|
||||
|
||||
await vi.runAllTimers()
|
||||
|
||||
expect(successSpy).not.toHaveBeenCalled()
|
||||
expect(failureSpy).not.toHaveBeenCalled(event.id, "hi")
|
||||
expect(completeSpy).toHaveBeenCalled()
|
||||
expect(timeoutSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("abort works", async () => {
|
||||
const sendSpy = vi.fn()
|
||||
const adapter = new MockAdapter('1', sendSpy)
|
||||
const signer = Nip01Signer.ephemeral()
|
||||
const event = await signer.sign(makeEvent(1))
|
||||
|
||||
const pub = unicast({
|
||||
relay: '1',
|
||||
context: {getAdapter: () => adapter},
|
||||
event,
|
||||
})
|
||||
|
||||
it("should handle failed publish", async () => {
|
||||
const pub = publish({event, relays})
|
||||
await vi.runAllTimersAsync()
|
||||
const successSpy = vi.fn()
|
||||
const failureSpy = vi.fn()
|
||||
const completeSpy = vi.fn()
|
||||
const abortSpy = vi.fn()
|
||||
|
||||
const onOk = mockExecutor.publish.mock.calls[0][1].onOk
|
||||
onOk("relay1", event.id, false, "failed")
|
||||
pub.on(PublishEventType.Success, successSpy)
|
||||
pub.on(PublishEventType.Failure, failureSpy)
|
||||
pub.on(PublishEventType.Complete, completeSpy)
|
||||
pub.on(PublishEventType.Timeout, abortSpy)
|
||||
|
||||
expect(pub.status.get("relay1")).toBe(PublishStatus.Failure)
|
||||
})
|
||||
await vi.runAllTimers(200)
|
||||
|
||||
it("should handle publish errors", async () => {
|
||||
const pub = publish({event, relays})
|
||||
await vi.runAllTimersAsync()
|
||||
expect(sendSpy).toHaveBeenCalledWith([ClientMessageType.Event, event])
|
||||
|
||||
const onError = mockExecutor.publish.mock.calls[0][1].onError
|
||||
onError("relay1")
|
||||
pub.abort()
|
||||
|
||||
expect(pub.status.get("relay1")).toBe(PublishStatus.Failure)
|
||||
})
|
||||
await vi.runAllTimers()
|
||||
|
||||
it("should handle timeout", async () => {
|
||||
const pub = publish({event, relays, timeout: 5000})
|
||||
await vi.runAllTimersAsync()
|
||||
|
||||
relays.forEach(relay => {
|
||||
expect(pub.status.get(relay)).toBe(PublishStatus.Timeout)
|
||||
})
|
||||
})
|
||||
|
||||
it("should handle abort signal", async () => {
|
||||
const controller = new AbortController()
|
||||
const pub = publish({event, relays, signal: controller.signal})
|
||||
await vi.advanceTimersToNextTimerAsync()
|
||||
|
||||
controller.abort()
|
||||
|
||||
relays.forEach(relay => {
|
||||
expect(pub.status.get(relay)).toBe(PublishStatus.Aborted)
|
||||
})
|
||||
})
|
||||
|
||||
it("should cleanup when all relays complete", async () => {
|
||||
const pub = publish({event, relays})
|
||||
await vi.runAllTimersAsync()
|
||||
|
||||
const onOk = mockExecutor.publish.mock.calls[0][1].onOk
|
||||
|
||||
// Complete all relays
|
||||
relays.forEach(relay => {
|
||||
onOk(relay, event.id, true, "success")
|
||||
})
|
||||
|
||||
expect(mockExecutorSub.unsubscribe).toHaveBeenCalled()
|
||||
expect(mockExecutor.target.cleanup).toHaveBeenCalled()
|
||||
expect(pub.result.resolve).toHaveBeenCalledWith(pub.status)
|
||||
})
|
||||
|
||||
it("should use custom verb if provided", () => {
|
||||
const pub = publish({event, relays, verb: "AUTH"})
|
||||
|
||||
expect(mockExecutor.publish.mock.calls[0][1].verb).toBe("AUTH")
|
||||
})
|
||||
|
||||
it("should use default timeout if not specified", async () => {
|
||||
const pub = publish({event, relays})
|
||||
|
||||
// Advance to default timeout
|
||||
await vi.advanceTimersByTimeAsync(10_000)
|
||||
|
||||
relays.forEach(relay => {
|
||||
expect(pub.status.get(relay)).toBe(PublishStatus.Timeout)
|
||||
})
|
||||
})
|
||||
expect(successSpy).not.toHaveBeenCalled()
|
||||
expect(failureSpy).not.toHaveBeenCalled(event.id, "hi")
|
||||
expect(completeSpy).toHaveBeenCalled()
|
||||
expect(abortSpy).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe("Multicast", () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it("should all basically work", async () => {
|
||||
const send1Spy = vi.fn()
|
||||
const adapter1 = new MockAdapter('1', send1Spy)
|
||||
const send2Spy = vi.fn()
|
||||
const adapter2 = new MockAdapter('2', send2Spy)
|
||||
const send3Spy = vi.fn()
|
||||
const adapter3 = new MockAdapter('3', send3Spy)
|
||||
const signer = Nip01Signer.ephemeral()
|
||||
const event = await signer.sign(makeEvent(1))
|
||||
|
||||
const pub = multicast({
|
||||
event,
|
||||
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}`)
|
||||
}
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const successSpy = vi.fn()
|
||||
const failureSpy = vi.fn()
|
||||
const completeSpy = vi.fn()
|
||||
const timeoutSpy = vi.fn()
|
||||
|
||||
pub.on(PublishEventType.Success, successSpy)
|
||||
pub.on(PublishEventType.Failure, failureSpy)
|
||||
pub.on(PublishEventType.Complete, completeSpy)
|
||||
pub.on(PublishEventType.Timeout, timeoutSpy)
|
||||
|
||||
adapter1.receive(["OK", event.id, true, "hi"])
|
||||
adapter2.receive(["OK", event.id, false, "hi"])
|
||||
|
||||
|
||||
await vi.runAllTimers()
|
||||
|
||||
expect(successSpy).toHaveBeenCalledWith(event.id, "hi", "1")
|
||||
expect(failureSpy).toHaveBeenCalledWith(event.id, "hi", "2")
|
||||
expect(completeSpy).toHaveBeenCalledTimes(1)
|
||||
expect(timeoutSpy).toHaveBeenCalledWith("3")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,244 +1,195 @@
|
||||
import {sleep} from "@welshman/lib"
|
||||
import WebSocket from "isomorphic-ws"
|
||||
import {afterEach, beforeEach, describe, expect, it, vi} from "vitest"
|
||||
import {ConnectionEvent} from "../src/ConnectionEvent"
|
||||
import {Message, Socket, SocketStatus} from "../src/Socket"
|
||||
import { sleep } from "@welshman/lib"
|
||||
import WebSocket from 'isomorphic-ws'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
|
||||
import { Socket, SocketStatus, SocketEventType } from "../src/socket"
|
||||
import { ClientMessage, RelayMessage } from "../src/message"
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock("isomorphic-ws")
|
||||
// vi.mock("@welshman/lib", async importOriginal => {
|
||||
// return {
|
||||
// ...(await importOriginal()),
|
||||
// // sleep: vi.fn().mockResolvedValue(undefined),
|
||||
// }
|
||||
// })
|
||||
vi.mock('isomorphic-ws', () => {
|
||||
const WebSocket = vi.fn(function () {
|
||||
setTimeout(() => this.onopen())
|
||||
})
|
||||
|
||||
WebSocket.prototype.send = vi.fn()
|
||||
|
||||
WebSocket.prototype.close = vi.fn(function () {
|
||||
this.onclose()
|
||||
})
|
||||
|
||||
return { default: WebSocket }
|
||||
})
|
||||
|
||||
describe("Socket", () => {
|
||||
let socket: Socket
|
||||
let mockConnection: any
|
||||
let mockWs: any
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
// Reset mocks
|
||||
vi.clearAllMocks()
|
||||
|
||||
// Setup mock connection
|
||||
mockConnection = {
|
||||
url: "wss://test.relay",
|
||||
emit: vi.fn(),
|
||||
}
|
||||
|
||||
// Setup mock WebSocket
|
||||
mockWs = {
|
||||
close: vi.fn(),
|
||||
send: vi.fn(),
|
||||
onopen: null,
|
||||
onclose: null,
|
||||
onerror: null,
|
||||
onmessage: null,
|
||||
}
|
||||
vi.mocked(WebSocket).mockImplementation(() => mockWs)
|
||||
|
||||
socket = new Socket(mockConnection)
|
||||
socket = new Socket("wss://test.relay")
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.useRealTimers()
|
||||
socket.cleanup()
|
||||
})
|
||||
|
||||
describe("initialization", () => {
|
||||
it("should initialize with New status", () => {
|
||||
expect(socket.status).toBe(SocketStatus.New)
|
||||
})
|
||||
|
||||
it("should setup worker handler", () => {
|
||||
const message = ["EVENT", {id: "123"}] as Message
|
||||
socket.worker.push(message)
|
||||
// workers batch messages every 50ms
|
||||
vi.advanceTimersByTime(50)
|
||||
|
||||
expect(mockConnection.emit).toHaveBeenCalledWith(ConnectionEvent.Receive, message)
|
||||
})
|
||||
it("should initialize with correct url", () => {
|
||||
expect(socket.url).toBe("wss://test.relay")
|
||||
})
|
||||
|
||||
describe("open", () => {
|
||||
it("should initialize WebSocket connection", async () => {
|
||||
socket.open()
|
||||
// wait for 2 timeout on wait
|
||||
await vi.advanceTimersByTimeAsync(10_000 * 2)
|
||||
expect(WebSocket).toHaveBeenCalledWith("wss://test.relay")
|
||||
expect(socket.status).toBe(SocketStatus.Opening)
|
||||
})
|
||||
it("should create websocket and emit opening status", () => {
|
||||
const statusSpy = vi.fn()
|
||||
socket.on(SocketEventType.Status, statusSpy)
|
||||
|
||||
// @check this test
|
||||
it("should handle successful connection", async () => {
|
||||
socket.open()
|
||||
await vi.advanceTimersByTimeAsync(10_000)
|
||||
|
||||
mockWs.onopen()
|
||||
|
||||
expect(socket.status).toBe(SocketStatus.Open)
|
||||
expect(mockConnection.emit).toHaveBeenCalledWith(ConnectionEvent.Open)
|
||||
})
|
||||
|
||||
it("should handle connection error (parallel)", async () => {
|
||||
await Promise.all([
|
||||
socket.open(),
|
||||
vi.advanceTimersByTimeAsync(1000),
|
||||
new Promise((resolve, reject) => setTimeout(() => resolve(mockWs.onerror()), 1000)),
|
||||
])
|
||||
|
||||
expect(socket.status).toBe(SocketStatus.Error)
|
||||
expect(socket.lastError).toBe(Date.now())
|
||||
expect(mockConnection.emit).toHaveBeenCalledWith(ConnectionEvent.Error)
|
||||
})
|
||||
|
||||
it("should retry after error timeout", async () => {
|
||||
// Simulate initial error
|
||||
socket.status = SocketStatus.Error
|
||||
socket.lastError = Date.now() - 16000 // More than 15 seconds ago
|
||||
|
||||
// @check awaiting socket open remains hanging as no socket callback is called
|
||||
// to change the socket status
|
||||
// await socket.open()
|
||||
socket.open()
|
||||
|
||||
await vi.advanceTimersToNextTimerAsync()
|
||||
expect(socket._ws).toBeDefined()
|
||||
expect(statusSpy).toHaveBeenCalledWith(SocketStatus.Opening, "wss://test.relay")
|
||||
|
||||
expect(WebSocket).toHaveBeenCalled()
|
||||
expect(mockConnection.emit).toHaveBeenCalledWith(ConnectionEvent.Reset)
|
||||
vi.runAllTimers()
|
||||
|
||||
expect(statusSpy).toHaveBeenCalledWith(SocketStatus.Open, "wss://test.relay")
|
||||
})
|
||||
|
||||
it("should not retry before error timeout", async () => {
|
||||
// Simulate recent error
|
||||
socket.status = SocketStatus.Error
|
||||
socket.lastError = Date.now() - 5000 // Less than 15 seconds ago
|
||||
it("should throw error if socket already exists", () => {
|
||||
socket.open()
|
||||
expect(() => socket.open()).toThrow("Attempted to open a websocket that has not been closed")
|
||||
})
|
||||
|
||||
await socket.open()
|
||||
it("should emit invalid status on invalid URL", () => {
|
||||
const statusSpy = vi.fn()
|
||||
socket.on(SocketEventType.Status, statusSpy)
|
||||
|
||||
expect(WebSocket).not.toHaveBeenCalled()
|
||||
vi.mocked(WebSocket).mockImplementationOnce(() => {
|
||||
throw new Error()
|
||||
})
|
||||
|
||||
socket.open()
|
||||
|
||||
expect(statusSpy).toHaveBeenCalledWith(SocketStatus.Invalid, "wss://test.relay")
|
||||
})
|
||||
})
|
||||
|
||||
describe("close", () => {
|
||||
it("should close WebSocket connection", async () => {
|
||||
socket.ws = mockWs
|
||||
socket.close()
|
||||
it("should close websocket and emit closed status", () => {
|
||||
const statusSpy = vi.fn()
|
||||
socket.on(SocketEventType.Status, statusSpy)
|
||||
|
||||
expect(mockWs.close).toHaveBeenCalled()
|
||||
expect(socket.ws).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should pause worker", async () => {
|
||||
const pauseSpy = vi.spyOn(socket.worker, "pause")
|
||||
socket.close()
|
||||
|
||||
expect(pauseSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should handle normal close", async () => {
|
||||
socket.open()
|
||||
await vi.advanceTimersToNextTimerAsync()
|
||||
mockWs.onclose()
|
||||
|
||||
expect(socket.status).toBe(SocketStatus.Closed)
|
||||
expect(mockConnection.emit).toHaveBeenCalledWith(ConnectionEvent.Close)
|
||||
const ws = socket._ws
|
||||
|
||||
socket.close()
|
||||
|
||||
expect(ws.close).toHaveBeenCalled()
|
||||
expect(statusSpy).toHaveBeenCalledWith(SocketStatus.Closed, "wss://test.relay")
|
||||
})
|
||||
})
|
||||
|
||||
describe("send", () => {
|
||||
it("should send message through WebSocket", async () => {
|
||||
const message = ["EVENT", {id: "123"}] as Message
|
||||
it("should queue messages and emit enqueue event", () => {
|
||||
const enqueueSpy = vi.fn()
|
||||
socket.on(SocketEventType.Enqueue, enqueueSpy)
|
||||
|
||||
// Setup open connection
|
||||
socket.open()
|
||||
await vi.advanceTimersToNextTimerAsync()
|
||||
mockWs.onopen()
|
||||
const message: ClientMessage = ["EVENT", { id: "123", kind: 1 }]
|
||||
socket.send(message)
|
||||
|
||||
await socket.send(message)
|
||||
|
||||
expect(mockWs.send).toHaveBeenCalledWith(JSON.stringify(message))
|
||||
expect(mockConnection.emit).toHaveBeenCalledWith(ConnectionEvent.Send, message)
|
||||
expect(enqueueSpy).toHaveBeenCalledWith(message, "wss://test.relay")
|
||||
})
|
||||
|
||||
it("should throw if no WebSocket available", () => {
|
||||
const message = ["EVENT", {id: "123"}] as Message
|
||||
socket.ws = undefined
|
||||
// unreachable code
|
||||
// expect(socket.send(message)).rejects.toThrow()
|
||||
it("should send messages when socket is open", async () => {
|
||||
const sendSpy = vi.fn()
|
||||
socket.on(SocketEventType.Send, sendSpy)
|
||||
|
||||
socket.open()
|
||||
socket._ws.onopen()
|
||||
|
||||
const message: ClientMessage = ["EVENT", { id: "123", kind: 1 }]
|
||||
socket.send(message)
|
||||
|
||||
await vi.runAllTimers()
|
||||
|
||||
expect(socket._ws.send).toHaveBeenCalledWith(JSON.stringify(message))
|
||||
expect(sendSpy).toHaveBeenCalledWith(message, "wss://test.relay")
|
||||
})
|
||||
})
|
||||
|
||||
describe("message handling", () => {
|
||||
it("should handle valid messages", async () => {
|
||||
const validMessage = ["EVENT", {id: "123"}]
|
||||
describe("receive", () => {
|
||||
it("should handle valid relay messages", async () => {
|
||||
const receiveSpy = vi.fn()
|
||||
socket.on(SocketEventType.Receive, receiveSpy)
|
||||
|
||||
socket.open()
|
||||
await vi.advanceTimersToNextTimerAsync()
|
||||
const message: RelayMessage = ["EVENT", "123", { id: "123", kind: 1 }]
|
||||
socket._ws.onmessage({ data: JSON.stringify(message) })
|
||||
|
||||
mockWs.onmessage({data: JSON.stringify(validMessage)})
|
||||
// Allow task queue to process
|
||||
await vi.runAllTimers()
|
||||
|
||||
await vi.advanceTimersToNextTimerAsync()
|
||||
|
||||
expect(mockConnection.emit).toHaveBeenCalledWith(ConnectionEvent.Receive, validMessage)
|
||||
expect(receiveSpy).toHaveBeenCalledWith(message, "wss://test.relay")
|
||||
})
|
||||
|
||||
it("should handle non-array messages", async () => {
|
||||
const invalidMessage = {type: "EVENT"}
|
||||
it("should emit error on invalid JSON", () => {
|
||||
const errorSpy = vi.fn()
|
||||
socket.on(SocketEventType.Error, errorSpy)
|
||||
|
||||
socket.open()
|
||||
await vi.advanceTimersToNextTimerAsync()
|
||||
mockWs.onmessage({data: JSON.stringify(invalidMessage)})
|
||||
socket._ws.onmessage({ data: "invalid json" })
|
||||
|
||||
expect(mockConnection.emit).toHaveBeenCalledWith(
|
||||
ConnectionEvent.InvalidMessage,
|
||||
JSON.stringify(invalidMessage),
|
||||
)
|
||||
expect(errorSpy).toHaveBeenCalledWith("Invalid message received", "wss://test.relay")
|
||||
})
|
||||
|
||||
it("should handle invalid JSON", async () => {
|
||||
const invalidJson = "invalid json"
|
||||
it("should emit error on non-array message", () => {
|
||||
const errorSpy = vi.fn()
|
||||
socket.on(SocketEventType.Error, errorSpy)
|
||||
|
||||
socket.open()
|
||||
await vi.advanceTimersToNextTimerAsync()
|
||||
mockWs.onmessage({data: invalidJson})
|
||||
socket._ws.onmessage({ data: JSON.stringify({ not: "an array" }) })
|
||||
|
||||
expect(mockConnection.emit).toHaveBeenCalledWith(ConnectionEvent.InvalidMessage, invalidJson)
|
||||
expect(errorSpy).toHaveBeenCalledWith("Invalid message received", "wss://test.relay")
|
||||
})
|
||||
})
|
||||
|
||||
describe("wait", () => {
|
||||
it("should wait for provisional states to resolve", async () => {
|
||||
socket.status = SocketStatus.Opening
|
||||
const waitPromise = socket.wait()
|
||||
describe("cleanup", () => {
|
||||
it("should close socket and clear queues", () => {
|
||||
socket.open()
|
||||
|
||||
// Change status after delay
|
||||
setTimeout(() => {
|
||||
socket.status = SocketStatus.Open
|
||||
}, 200)
|
||||
const ws = socket._ws
|
||||
|
||||
await vi.advanceTimersByTimeAsync(200)
|
||||
await waitPromise
|
||||
socket.cleanup()
|
||||
|
||||
expect(socket.status).toBe(SocketStatus.Open)
|
||||
expect(ws.close).toHaveBeenCalled()
|
||||
expect(socket.listenerCount(SocketEventType.Send)).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe("error handling", () => {
|
||||
it("should handle invalid URLs", async () => {
|
||||
vi.mocked(WebSocket).mockImplementationOnce(() => {
|
||||
throw new Error("Invalid URL")
|
||||
})
|
||||
it("should emit error status on websocket error", () => {
|
||||
const statusSpy = vi.fn()
|
||||
socket.on(SocketEventType.Status, statusSpy)
|
||||
|
||||
const now = Date.now()
|
||||
vi.setSystemTime(now)
|
||||
socket.open()
|
||||
socket._ws.onerror()
|
||||
|
||||
await socket.open()
|
||||
expect(statusSpy).toHaveBeenCalledWith(SocketStatus.Error, "wss://test.relay")
|
||||
})
|
||||
})
|
||||
|
||||
expect(socket.status).toBe(SocketStatus.Invalid)
|
||||
expect(socket.lastError).toBe(now)
|
||||
expect(mockConnection.emit).toHaveBeenCalledWith(ConnectionEvent.InvalidUrl)
|
||||
describe("attemptToOpen", () => {
|
||||
it("should open socket if not already open", () => {
|
||||
const openSpy = vi.spyOn(socket, "open")
|
||||
|
||||
socket.attemptToOpen()
|
||||
expect(openSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should not open socket if already open", () => {
|
||||
const openSpy = vi.spyOn(socket, "open")
|
||||
|
||||
socket.open()
|
||||
socket.attemptToOpen()
|
||||
|
||||
expect(openSpy).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,273 +0,0 @@
|
||||
import {diff, pull, push, sync, pullWithoutNegentropy, pushWithoutNegentropy} from "../src/Sync"
|
||||
import {ctx, now} from "@welshman/lib"
|
||||
import type {SignedEvent, TrustedEvent, Filter} from "@welshman/util"
|
||||
import {vi, describe, it, expect, beforeEach} from "vitest"
|
||||
import {subscribe} from "../src/Subscribe"
|
||||
import {publish} from "../src/Publish"
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock("../src/Subscribe", () => ({
|
||||
subscribe: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock("../src/Publish", () => ({
|
||||
publish: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock("@welshman/lib", async importOriginal => {
|
||||
return {
|
||||
...(await importOriginal()),
|
||||
now: vi.fn().mockReturnValue(1000),
|
||||
}
|
||||
})
|
||||
|
||||
describe("Sync", () => {
|
||||
let mockExecutor: any
|
||||
let mockDiffSub: any
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
|
||||
mockDiffSub = {unsubscribe: vi.fn()}
|
||||
mockExecutor = {
|
||||
diff: vi.fn().mockImplementation((filter, events, {onMessage, onClose}) => {
|
||||
// Simulate diff message
|
||||
onMessage("relay1", {have: ["id1"], need: ["id2"]})
|
||||
onClose()
|
||||
return mockDiffSub
|
||||
}),
|
||||
target: {
|
||||
cleanup: vi.fn(),
|
||||
},
|
||||
}
|
||||
|
||||
ctx.net = {
|
||||
...ctx.net,
|
||||
getExecutor: vi.fn().mockReturnValue(mockExecutor),
|
||||
}
|
||||
|
||||
// Mock subscribe to simulate event reception
|
||||
vi.mocked(subscribe).mockImplementation(({onEvent, onClose, onComplete}) => {
|
||||
if (onEvent) {
|
||||
onEvent({id: "id2", created_at: 900} as TrustedEvent)
|
||||
}
|
||||
onClose?.("relay1")
|
||||
onComplete?.()
|
||||
return {close: vi.fn()}
|
||||
})
|
||||
|
||||
// Mock publish to return resolved result
|
||||
vi.mocked(publish).mockImplementation(() => ({
|
||||
result: Promise.resolve(new Map()),
|
||||
id: "pub1",
|
||||
created_at: 1000,
|
||||
emitter: {} as any,
|
||||
request: {} as any,
|
||||
status: new Map(),
|
||||
}))
|
||||
})
|
||||
|
||||
describe("diff", () => {
|
||||
it("should aggregate diff results by relay", async () => {
|
||||
const result = await diff({
|
||||
relays: ["relay1", "relay2"],
|
||||
filters: [{kinds: [1]}],
|
||||
events: [{id: "id1"} as TrustedEvent],
|
||||
})
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
relay: "relay1",
|
||||
have: ["id1"],
|
||||
need: ["id2"],
|
||||
},
|
||||
{
|
||||
relay: "relay2",
|
||||
have: ["id1"],
|
||||
need: ["id2"],
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it("should handle multiple filters", async () => {
|
||||
const result = await diff({
|
||||
relays: ["relay1"],
|
||||
filters: [{kinds: [1]}, {kinds: [2]}],
|
||||
events: [{id: "id1"} as TrustedEvent],
|
||||
})
|
||||
|
||||
expect(mockExecutor.diff).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it("should handle diff errors", async () => {
|
||||
mockExecutor.diff.mockImplementation((filter, events, {onError}) => {
|
||||
onError("relay1", "error message")
|
||||
return mockDiffSub
|
||||
})
|
||||
|
||||
await expect(
|
||||
diff({
|
||||
relays: ["relay1"],
|
||||
filters: [{kinds: [1]}],
|
||||
events: [],
|
||||
}),
|
||||
).rejects.toEqual("error message")
|
||||
})
|
||||
})
|
||||
|
||||
describe("pull", () => {
|
||||
it("should pull needed events", async () => {
|
||||
const onEvent = vi.fn()
|
||||
const result = await pull({
|
||||
relays: ["relay1"],
|
||||
filters: [{kinds: [1]}],
|
||||
events: [],
|
||||
onEvent,
|
||||
})
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].id).toBe("id2")
|
||||
expect(onEvent).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should limit duplicate pulls", async () => {
|
||||
// Mock diff to return same need from multiple relays
|
||||
mockExecutor.diff.mockImplementation((filter, events, {onMessage, onClose}) => {
|
||||
onMessage("relay1", {have: [], need: ["id2"]})
|
||||
onClose()
|
||||
return mockDiffSub
|
||||
})
|
||||
|
||||
await pull({
|
||||
relays: ["relay1", "relay2", "relay3"],
|
||||
filters: [{kinds: [1]}],
|
||||
events: [],
|
||||
})
|
||||
|
||||
// Should only subscribe maximum twice for the same ID
|
||||
expect(subscribe).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it("should chunk large ID lists", async () => {
|
||||
const manyIds = Array.from({length: 2000}, (_, i) => `id${i}`)
|
||||
mockExecutor.diff.mockImplementation((filter, events, {onMessage, onClose}) => {
|
||||
onMessage("relay1", {have: [], need: manyIds})
|
||||
onClose()
|
||||
return mockDiffSub
|
||||
})
|
||||
|
||||
await pull({
|
||||
relays: ["relay1"],
|
||||
filters: [{kinds: [1]}],
|
||||
events: [],
|
||||
})
|
||||
|
||||
// Should split into chunks of 1024
|
||||
expect(subscribe).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
})
|
||||
|
||||
describe("push", () => {
|
||||
it("should push events to relays that have them", async () => {
|
||||
await push({
|
||||
relays: ["relay1"],
|
||||
filters: [{kinds: [1]}],
|
||||
events: [{id: "id1"} as SignedEvent],
|
||||
})
|
||||
|
||||
expect(publish).toHaveBeenCalledWith({
|
||||
event: expect.any(Object),
|
||||
relays: ["relay1"],
|
||||
})
|
||||
})
|
||||
|
||||
it("should skip events with no matching relays", async () => {
|
||||
mockExecutor.diff.mockImplementation((filter, events, {onMessage, onClose}) => {
|
||||
onMessage("relay1", {have: [], need: []})
|
||||
onClose()
|
||||
return mockDiffSub
|
||||
})
|
||||
|
||||
await push({
|
||||
relays: ["relay1"],
|
||||
filters: [{kinds: [1]}],
|
||||
events: [{id: "id1"} as SignedEvent],
|
||||
})
|
||||
|
||||
expect(publish).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe("sync", () => {
|
||||
it("should perform pull and push operations", async () => {
|
||||
await sync({
|
||||
relays: ["relay1"],
|
||||
filters: [{kinds: [1]}],
|
||||
events: [{id: "id1"} as SignedEvent],
|
||||
})
|
||||
|
||||
expect(subscribe).toHaveBeenCalled()
|
||||
expect(publish).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe("pullWithoutNegentropy", () => {
|
||||
it("should pull events until no more results", async () => {
|
||||
let callCount = 0
|
||||
vi.mocked(subscribe).mockImplementation(({onEvent, onComplete}) => {
|
||||
if (callCount++ < 2) {
|
||||
onEvent?.({id: `id${callCount}`, created_at: 900} as TrustedEvent)
|
||||
}
|
||||
onComplete?.()
|
||||
return {close: vi.fn()}
|
||||
})
|
||||
|
||||
const result = await pullWithoutNegentropy({
|
||||
relays: ["relay1"],
|
||||
filters: [{kinds: [1]}],
|
||||
})
|
||||
|
||||
expect(result).toHaveLength(2)
|
||||
expect(subscribe).toHaveBeenCalledTimes(3) // 2 with results + 1 final check
|
||||
})
|
||||
|
||||
it("should update until timestamp based on events", async () => {
|
||||
let callCount = 0
|
||||
vi.mocked(subscribe).mockImplementation(({onEvent, onComplete}) => {
|
||||
if (!callCount) {
|
||||
onEvent?.({id: "id1", created_at: 500} as TrustedEvent)
|
||||
callCount++
|
||||
}
|
||||
onComplete?.()
|
||||
return {close: vi.fn()}
|
||||
})
|
||||
|
||||
await pullWithoutNegentropy({
|
||||
relays: ["relay1"],
|
||||
filters: [{kinds: [1]}],
|
||||
})
|
||||
|
||||
// Second subscription should use updated until
|
||||
expect(subscribe).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
filters: expect.arrayContaining([expect.objectContaining({until: 499})]),
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("pushWithoutNegentropy", () => {
|
||||
it("should push all events to all relays", async () => {
|
||||
await pushWithoutNegentropy({
|
||||
relays: ["relay1", "relay2"],
|
||||
events: [{id: "id1"} as SignedEvent, {id: "id2"} as SignedEvent],
|
||||
})
|
||||
|
||||
expect(publish).toHaveBeenCalledTimes(2)
|
||||
expect(publish).toHaveBeenCalledWith({
|
||||
event: expect.any(Object),
|
||||
relays: ["relay1", "relay2"],
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,170 @@
|
||||
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest"
|
||||
import { Socket, SocketEventType } from "../src/socket"
|
||||
import { Relay, LOCAL_RELAY_URL, isRelayUrl } from "@welshman/util"
|
||||
import { AdapterEventType, SocketAdapter, LocalAdapter, getAdapter } from "../src/adapter"
|
||||
import { Pool } from "../src/pool"
|
||||
import { ClientMessage, RelayMessage } from "../src/message"
|
||||
import EventEmitter from "events"
|
||||
|
||||
vi.mock('isomorphic-ws', () => {
|
||||
const WebSocket = vi.fn(function () {
|
||||
setTimeout(() => this.onopen())
|
||||
})
|
||||
|
||||
WebSocket.prototype.send = vi.fn()
|
||||
|
||||
WebSocket.prototype.close = vi.fn(function () {
|
||||
this.onclose()
|
||||
})
|
||||
|
||||
return { default: WebSocket }
|
||||
})
|
||||
|
||||
describe("SocketAdapter", () => {
|
||||
let socket: Socket
|
||||
let adapter: SocketAdapter
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
socket = new Socket('wss://test.relay')
|
||||
adapter = new SocketAdapter(socket)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.useRealTimers()
|
||||
socket.cleanup()
|
||||
adapter.cleanup()
|
||||
})
|
||||
|
||||
it("should initialize with correct socket", () => {
|
||||
expect(adapter.socket).toBe(socket)
|
||||
expect(adapter.urls).toEqual(["wss://test.relay"])
|
||||
expect(adapter.sockets).toEqual([socket])
|
||||
})
|
||||
|
||||
it("should forward received messages", () => {
|
||||
const receiveSpy = vi.fn()
|
||||
adapter.on(AdapterEventType.Receive, receiveSpy)
|
||||
|
||||
const message: RelayMessage = ["EVENT", "123", { id: "123", kind: 1 }]
|
||||
socket.emit(SocketEventType.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 }]
|
||||
adapter.send(message)
|
||||
|
||||
expect(sendSpy).toHaveBeenCalledWith(message)
|
||||
})
|
||||
|
||||
it("should cleanup properly", () => {
|
||||
const removeListenersSpy = vi.spyOn(adapter, "removeAllListeners")
|
||||
adapter.cleanup()
|
||||
expect(removeListenersSpy).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe("LocalAdapter", () => {
|
||||
let relay: Relay & EventEmitter
|
||||
let adapter: LocalAdapter
|
||||
|
||||
beforeEach(() => {
|
||||
const mockRelay = new EventEmitter()
|
||||
Object.assign(mockRelay, {
|
||||
send: vi.fn(),
|
||||
removeAllListeners: vi.fn()
|
||||
})
|
||||
relay = mockRelay as unknown as Relay & EventEmitter
|
||||
adapter = new LocalAdapter(relay)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
adapter.cleanup()
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it("should initialize with correct relay", () => {
|
||||
expect(adapter.relay).toBe(relay)
|
||||
expect(adapter.urls).toEqual([LOCAL_RELAY_URL])
|
||||
expect(adapter.sockets).toEqual([])
|
||||
})
|
||||
|
||||
it("should forward received messages", () => {
|
||||
const receiveSpy = vi.fn()
|
||||
adapter.on(AdapterEventType.Receive, receiveSpy)
|
||||
|
||||
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 }]
|
||||
adapter.send(message)
|
||||
|
||||
expect(relay.send).toHaveBeenCalledWith("EVENT", message[1])
|
||||
})
|
||||
|
||||
it("should cleanup properly", () => {
|
||||
const removeListenersSpy = vi.spyOn(adapter, "removeAllListeners")
|
||||
adapter.cleanup()
|
||||
expect(removeListenersSpy).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe("getAdapter", () => {
|
||||
let pool: Pool
|
||||
let relay: Relay
|
||||
|
||||
beforeEach(() => {
|
||||
pool = new Pool()
|
||||
relay = new Relay()
|
||||
pool.get = vi.fn().mockReturnValue(new Socket("wss://test.relay"))
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it("should return LocalAdapter for local relay URL", () => {
|
||||
const url = LOCAL_RELAY_URL
|
||||
const adapter = getAdapter(url, { relay })
|
||||
expect(adapter).toBeInstanceOf(LocalAdapter)
|
||||
})
|
||||
|
||||
it("should return SocketAdapter for remote relay URL", () => {
|
||||
const url = "wss://test.relay"
|
||||
const adapter = getAdapter(url, { pool })
|
||||
expect(adapter).toBeInstanceOf(SocketAdapter)
|
||||
})
|
||||
|
||||
it("should throw error for invalid relay URL", () => {
|
||||
expect(() => getAdapter("invalid-url", {})).toThrow("Invalid relay url invalid-url")
|
||||
})
|
||||
|
||||
it("should throw error for local relay URL without relay context", () => {
|
||||
const url = LOCAL_RELAY_URL
|
||||
expect(() => getAdapter(url, {})).toThrow(`Unable to get local relay for ${url}`)
|
||||
})
|
||||
|
||||
it("should throw error for remote relay URL without pool context", () => {
|
||||
const url = "wss://test.relay"
|
||||
expect(() => getAdapter(url, {})).toThrow(`Unable to get socket for ${url}`)
|
||||
})
|
||||
|
||||
it("should use custom adapter if provided", () => {
|
||||
const customAdapter = new SocketAdapter(new Socket("wss://test.relay"))
|
||||
const getCustomAdapter = vi.fn().mockReturnValue(customAdapter)
|
||||
const url = "wss://test.relay"
|
||||
|
||||
const adapter = getAdapter(url, { getAdapter: getCustomAdapter })
|
||||
|
||||
expect(getCustomAdapter).toHaveBeenCalledWith(url, { getAdapter: getCustomAdapter })
|
||||
expect(adapter).toBe(customAdapter)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,204 @@
|
||||
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest"
|
||||
import { Socket, SocketStatus, SocketEventType } from "../src/socket"
|
||||
import { makeEvent, CLIENT_AUTH } from "@welshman/util"
|
||||
import { Nip01Signer } from "@welshman/signer"
|
||||
import { AuthState, AuthStatus, AuthStateEventType, AuthManager, makeAuthEvent } from "../src/auth"
|
||||
import EventEmitter from "events"
|
||||
import { RelayMessage } from "../src/message"
|
||||
|
||||
vi.mock('isomorphic-ws', () => {
|
||||
const WebSocket = vi.fn(function () {
|
||||
setTimeout(() => this.onopen())
|
||||
})
|
||||
|
||||
WebSocket.prototype.send = vi.fn()
|
||||
|
||||
WebSocket.prototype.close = vi.fn(function () {
|
||||
this.onclose()
|
||||
})
|
||||
|
||||
return { default: WebSocket }
|
||||
})
|
||||
|
||||
describe('auth', () => {
|
||||
let socket: Socket
|
||||
let authManager: AuthManager
|
||||
let sign = vi.fn(Nip01Signer.ephemeral().sign)
|
||||
|
||||
beforeEach(() => {
|
||||
socket = new Socket('wss://test.relay')
|
||||
authManager = new AuthManager(socket, { sign })
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks()
|
||||
socket.cleanup()
|
||||
authManager.cleanup()
|
||||
})
|
||||
|
||||
describe("AuthState", () => {
|
||||
it("should initialize with None status", () => {
|
||||
expect(authManager.state.status).toBe(AuthStatus.None)
|
||||
})
|
||||
|
||||
it("should handle AUTH message from relay", () => {
|
||||
const message: RelayMessage = ["AUTH", "challenge123"]
|
||||
socket.emit(SocketEventType.Receive, message)
|
||||
|
||||
expect(authManager.state.challenge).toBe("challenge123")
|
||||
expect(authManager.state.status).toBe(AuthStatus.Requested)
|
||||
})
|
||||
|
||||
it("should handle successful OK message", () => {
|
||||
authManager.state.request = "request123"
|
||||
const message: RelayMessage = ["OK", "request123", true, "success"]
|
||||
socket.emit(SocketEventType.Receive, message)
|
||||
|
||||
expect(authManager.state.status).toBe(AuthStatus.Ok)
|
||||
expect(authManager.state.details).toBe("success")
|
||||
})
|
||||
|
||||
it("should handle failed OK message", () => {
|
||||
authManager.state.request = "request123"
|
||||
const message: RelayMessage = ["OK", "request123", false, "forbidden"]
|
||||
socket.emit(SocketEventType.Receive, message)
|
||||
|
||||
expect(authManager.state.status).toBe(AuthStatus.Forbidden)
|
||||
expect(authManager.state.details).toBe("forbidden")
|
||||
})
|
||||
|
||||
it("should ignore OK messages for different requests", () => {
|
||||
authManager.state.request = "request123"
|
||||
const message: RelayMessage = ["OK", "different-request", true, "success"]
|
||||
socket.emit(SocketEventType.Receive, message)
|
||||
|
||||
expect(authManager.state.status).toBe(AuthStatus.None)
|
||||
})
|
||||
|
||||
it("should handle client AUTH message", () => {
|
||||
const message: RelayMessage = ["AUTH", { id: "123", kind: CLIENT_AUTH }]
|
||||
socket.emit(SocketEventType.Enqueue, message)
|
||||
|
||||
expect(authManager.state.status).toBe(AuthStatus.PendingResponse)
|
||||
})
|
||||
|
||||
it("should reset state on socket close", () => {
|
||||
authManager.state.challenge = "challenge123"
|
||||
authManager.state.request = "request123"
|
||||
authManager.state.details = "details"
|
||||
authManager.state.status = AuthStatus.PendingResponse
|
||||
|
||||
socket.emit(SocketEventType.Status, SocketStatus.Closed)
|
||||
|
||||
expect(authManager.state.challenge).toBeUndefined()
|
||||
expect(authManager.state.request).toBeUndefined()
|
||||
expect(authManager.state.details).toBeUndefined()
|
||||
expect(authManager.state.status).toBe(AuthStatus.None)
|
||||
})
|
||||
|
||||
it("should emit status changes", () => {
|
||||
const statusSpy = vi.fn()
|
||||
authManager.state.on(AuthStateEventType.Status, statusSpy)
|
||||
|
||||
authManager.state.setStatus(AuthStatus.Requested)
|
||||
|
||||
expect(statusSpy).toHaveBeenCalledWith(AuthStatus.Requested)
|
||||
})
|
||||
|
||||
it("should cleanup properly", () => {
|
||||
const removeListenersSpy = vi.spyOn(authManager.state, "removeAllListeners")
|
||||
authManager.state.cleanup()
|
||||
expect(removeListenersSpy).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe("AuthManager", () => {
|
||||
it("should create AuthState instance", () => {
|
||||
expect(authManager.state).toBeInstanceOf(AuthState)
|
||||
})
|
||||
|
||||
it("should respond automatically when eager is true", () => {
|
||||
const respondSpy = vi.spyOn(AuthManager.prototype, "respond")
|
||||
const eagerManager = new AuthManager(socket, { sign, eager: true })
|
||||
|
||||
socket.emit(SocketEventType.Receive, ["AUTH", "challenge123"])
|
||||
|
||||
expect(respondSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should not respond automatically when eager is false", () => {
|
||||
const respondSpy = vi.spyOn(AuthManager.prototype, "respond")
|
||||
socket.emit(SocketEventType.Receive, ["AUTH", "challenge123"])
|
||||
|
||||
expect(respondSpy).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
describe("respond", () => {
|
||||
it("should throw error if no challenge", async () => {
|
||||
await expect(authManager.respond()).rejects.toThrow("Attempted to authenticate with no challenge")
|
||||
})
|
||||
|
||||
it("should throw error if status is not Requested", async () => {
|
||||
authManager.state.challenge = "challenge123"
|
||||
authManager.state.status = AuthStatus.PendingSignature
|
||||
|
||||
await expect(authManager.respond()).rejects.toThrow("Attempted to authenticate when auth is already auth:status:pending_signature")
|
||||
})
|
||||
|
||||
it("should handle successful sign", async () => {
|
||||
const sendSpy = vi.spyOn(socket, 'send')
|
||||
|
||||
authManager.state.challenge = "challenge123"
|
||||
authManager.state.status = AuthStatus.Requested
|
||||
const signedEvent = { id: "signed-event-id", kind: CLIENT_AUTH }
|
||||
sign.mockResolvedValue(signedEvent)
|
||||
|
||||
await authManager.respond()
|
||||
|
||||
expect(authManager.state.request).toBe("signed-event-id")
|
||||
expect(sendSpy).toHaveBeenCalledWith(["AUTH", signedEvent])
|
||||
})
|
||||
|
||||
it("should handle denied signature", async () => {
|
||||
const sendSpy = vi.spyOn(socket, 'send')
|
||||
|
||||
authManager.state.challenge = "challenge123"
|
||||
authManager.state.status = AuthStatus.Requested
|
||||
sign.mockResolvedValue(null)
|
||||
|
||||
await authManager.respond()
|
||||
|
||||
expect(authManager.state.status).toBe(AuthStatus.DeniedSignature)
|
||||
expect(sendSpy).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe("attempt", () => {
|
||||
it("should attempt to open socket", async () => {
|
||||
const attemptToOpenSpy = vi.spyOn(socket, 'attemptToOpen')
|
||||
await authManager.attempt()
|
||||
expect(attemptToOpenSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should wait for challenge", async () => {
|
||||
const waitForChallengeSpy = vi.spyOn(authManager, "waitForChallenge")
|
||||
await authManager.attempt()
|
||||
expect(waitForChallengeSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should respond if challenge received", async () => {
|
||||
const respondSpy = vi.spyOn(authManager, "respond")
|
||||
authManager.state.challenge = "challenge123"
|
||||
authManager.state.status = AuthStatus.Requested
|
||||
await authManager.attempt()
|
||||
expect(respondSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should wait for resolution", async () => {
|
||||
const waitForResolutionSpy = vi.spyOn(authManager, "waitForResolution")
|
||||
await authManager.attempt()
|
||||
expect(waitForResolutionSpy).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,466 @@
|
||||
import { AUTH_JOIN } from "@welshman/util"
|
||||
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest"
|
||||
import { Socket, SocketStatus, SocketEventType } from "../src/socket"
|
||||
import { AuthStatus, AuthStateEventType } from "../src/auth"
|
||||
import {
|
||||
socketPolicySendWhenOpen,
|
||||
socketPolicyDeferOnAuth,
|
||||
socketPolicyRetryAuthRequired,
|
||||
socketPolicyConnectOnSend,
|
||||
socketPolicyCloseOnTimeout,
|
||||
socketPolicyReopenActive
|
||||
} from "../src/policy"
|
||||
import { ClientMessage, RelayMessage } from "../src/message"
|
||||
|
||||
// Hoist mock definition to top level
|
||||
const mockWs = vi.hoisted(() => ({
|
||||
close: vi.fn(),
|
||||
send: vi.fn(),
|
||||
onopen: vi.fn(),
|
||||
onclose: null,
|
||||
onerror: null,
|
||||
onmessage: null,
|
||||
}))
|
||||
|
||||
// Mock the WebSocket module
|
||||
vi.mock('isomorphic-ws', () => ({
|
||||
default: mockWs
|
||||
}))
|
||||
|
||||
describe('policy', () => {
|
||||
let socket: Socket
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
socket = new Socket("wss://test.relay")
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
socket.cleanup()
|
||||
vi.useRealTimers()
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe("socketPolicyDeferOnAuth", () => {
|
||||
it("should buffer messages when not authenticated", () => {
|
||||
const cleanup = socketPolicyDeferOnAuth(socket)
|
||||
const removeSpy = vi.spyOn(socket._sendQueue, 'remove')
|
||||
|
||||
socket.emit(SocketEventType.Receive, ["AUTH", "challenge"])
|
||||
|
||||
// Regular event should be buffered
|
||||
const event: ClientMessage = ["EVENT", { id: "123"}]
|
||||
socket.send(event)
|
||||
expect(removeSpy).toHaveBeenCalledWith(event)
|
||||
|
||||
// Auth event should not be buffered
|
||||
const authEvent: ClientMessage = ["AUTH", { id: "456" }]
|
||||
socket.send(authEvent)
|
||||
expect(removeSpy).not.toHaveBeenCalledWith(authEvent)
|
||||
|
||||
// Auth join event should not be buffered
|
||||
const joinEvent: ClientMessage = ["EVENT", { id: "789", kind: AUTH_JOIN }]
|
||||
socket.send(joinEvent)
|
||||
expect(removeSpy).not.toHaveBeenCalledWith(joinEvent)
|
||||
|
||||
cleanup()
|
||||
})
|
||||
|
||||
it("should send buffered messages when auth succeeds", () => {
|
||||
const cleanup = socketPolicyDeferOnAuth(socket)
|
||||
const sendSpy = vi.spyOn(socket, 'send')
|
||||
|
||||
socket.emit(SocketEventType.Receive, ["AUTH", "challenge"])
|
||||
|
||||
// Buffer some messages
|
||||
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.emit(AuthStateEventType.Status, AuthStatus.Ok)
|
||||
|
||||
expect(sendSpy).toHaveBeenCalledWith(event1)
|
||||
expect(sendSpy).toHaveBeenCalledWith(event2)
|
||||
|
||||
cleanup()
|
||||
})
|
||||
|
||||
it("should handle CLOSE messages properly", () => {
|
||||
const cleanup = socketPolicyDeferOnAuth(socket)
|
||||
const removeSpy = vi.spyOn(socket._sendQueue, 'remove')
|
||||
|
||||
socket.emit(SocketEventType.Receive, ["AUTH", "challenge"])
|
||||
|
||||
// Buffer a REQ message
|
||||
const req: ClientMessage = ["REQ", "123", { kinds: [1] }]
|
||||
socket.send(req)
|
||||
|
||||
// Send CLOSE for buffered REQ
|
||||
const close: ClientMessage = ["CLOSE", "123"]
|
||||
socket.send(close)
|
||||
|
||||
// Both messages should be removed
|
||||
expect(removeSpy).toHaveBeenCalledWith(req)
|
||||
expect(removeSpy).toHaveBeenCalledWith(close)
|
||||
|
||||
cleanup()
|
||||
})
|
||||
})
|
||||
|
||||
describe("socketPolicyRetryAuthRequired", () => {
|
||||
it("should retry events once when auth-required", () => {
|
||||
const cleanup = socketPolicyRetryAuthRequired(socket)
|
||||
const sendSpy = vi.spyOn(socket, 'send')
|
||||
|
||||
// Send an event
|
||||
const event: ClientMessage = ["EVENT", { id: "123", kind: 1, content: "", tags: [], pubkey: "", sig: "" }]
|
||||
socket.emit(SocketEventType.Send, event)
|
||||
|
||||
// Receive auth-required rejection
|
||||
socket.emit(SocketEventType.Receive, ["OK", "123", false, "auth-required: need to auth first"])
|
||||
|
||||
// Should retry the event
|
||||
expect(sendSpy).toHaveBeenCalledWith(event)
|
||||
|
||||
// Receive another auth-required rejection
|
||||
socket.emit(SocketEventType.Receive, ["OK", "123", false, "auth-required: need to auth first"])
|
||||
|
||||
// Should not retry again
|
||||
expect(sendSpy).toHaveBeenCalledTimes(1)
|
||||
|
||||
cleanup()
|
||||
})
|
||||
|
||||
it("should retry REQ once when auth-required", () => {
|
||||
const cleanup = socketPolicyRetryAuthRequired(socket)
|
||||
const sendSpy = vi.spyOn(socket, 'send')
|
||||
|
||||
// Send a REQ
|
||||
const req: ClientMessage = ["REQ", "123", { kinds: [1] }]
|
||||
socket.emit(SocketEventType.Send, req)
|
||||
|
||||
// Receive auth-required rejection via CLOSED
|
||||
socket.emit(SocketEventType.Receive, ["CLOSED", "123", "auth-required: need to auth first"])
|
||||
|
||||
// Should retry the request
|
||||
expect(sendSpy).toHaveBeenCalledWith(req)
|
||||
|
||||
// Receive another auth-required rejection
|
||||
socket.emit(SocketEventType.Receive, ["CLOSED", "123", "auth-required: need to auth first"])
|
||||
|
||||
// Should not retry again
|
||||
expect(sendSpy).toHaveBeenCalledTimes(1)
|
||||
|
||||
cleanup()
|
||||
})
|
||||
|
||||
it("should not retry AUTH_JOIN events", () => {
|
||||
const cleanup = socketPolicyRetryAuthRequired(socket)
|
||||
const sendSpy = vi.spyOn(socket, 'send')
|
||||
|
||||
// Send an AUTH_JOIN event
|
||||
const event: ClientMessage = ["EVENT", { id: "123", kind: AUTH_JOIN, content: "", tags: [], pubkey: "", sig: "" }]
|
||||
socket.emit(SocketEventType.Send, event)
|
||||
|
||||
// Receive auth-required rejection
|
||||
socket.emit(SocketEventType.Receive, ["OK", "123", false, "auth-required: need to auth first"])
|
||||
|
||||
// Should not retry AUTH_JOIN events
|
||||
expect(sendSpy).not.toHaveBeenCalled()
|
||||
|
||||
cleanup()
|
||||
})
|
||||
|
||||
it("should clear pending messages on successful response", () => {
|
||||
const cleanup = socketPolicyRetryAuthRequired(socket)
|
||||
const sendSpy = vi.spyOn(socket, 'send')
|
||||
|
||||
// Send an event
|
||||
const event: ClientMessage = ["EVENT", { id: "123", kind: 1, content: "", tags: [], pubkey: "", sig: "" }]
|
||||
socket.emit(SocketEventType.Send, event)
|
||||
|
||||
// Receive successful response
|
||||
socket.emit(SocketEventType.Receive, ["OK", "123", true, ""])
|
||||
|
||||
// Receive auth-required rejection (should not trigger retry since message was cleared)
|
||||
socket.emit(SocketEventType.Receive, ["OK", "123", false, "auth-required: need to auth first"])
|
||||
|
||||
// Should not retry
|
||||
expect(sendSpy).not.toHaveBeenCalled()
|
||||
|
||||
cleanup()
|
||||
})
|
||||
})
|
||||
|
||||
describe("socketPolicyConnectOnSend", () => {
|
||||
it("should open socket on send when closed", () => {
|
||||
const cleanup = socketPolicyConnectOnSend(socket)
|
||||
const openSpy = vi.spyOn(socket, 'open')
|
||||
|
||||
// Socket starts closed
|
||||
socket.emit(SocketEventType.Status, SocketStatus.Closed)
|
||||
|
||||
// Send a message
|
||||
const event: ClientMessage = ["EVENT", { id: "123", kind: 1 }]
|
||||
socket.emit(SocketEventType.Enqueue, event)
|
||||
|
||||
// Should open the socket
|
||||
expect(openSpy).toHaveBeenCalled()
|
||||
|
||||
cleanup()
|
||||
})
|
||||
|
||||
it("should not open socket if already open", () => {
|
||||
const cleanup = socketPolicyConnectOnSend(socket)
|
||||
const openSpy = vi.spyOn(socket, 'open')
|
||||
|
||||
// Socket is open
|
||||
socket.emit(SocketEventType.Status, SocketStatus.Open)
|
||||
|
||||
// Send a message
|
||||
const event: ClientMessage = ["EVENT", { id: "123", kind: 1 }]
|
||||
socket.emit(SocketEventType.Enqueue, event)
|
||||
|
||||
// Should not try to open the socket
|
||||
expect(openSpy).not.toHaveBeenCalled()
|
||||
|
||||
cleanup()
|
||||
})
|
||||
|
||||
it("should not open socket if there was a recent error", () => {
|
||||
const cleanup = socketPolicyConnectOnSend(socket)
|
||||
const openSpy = vi.spyOn(socket, 'open')
|
||||
|
||||
// Socket has an error
|
||||
socket.emit(SocketEventType.Status, SocketStatus.Error)
|
||||
socket.emit(SocketEventType.Status, SocketStatus.Closed)
|
||||
|
||||
// Send a message
|
||||
const event: ClientMessage = ["EVENT", { id: "123", kind: 1 }]
|
||||
socket.emit(SocketEventType.Enqueue, event)
|
||||
|
||||
// Should not try to open the socket due to recent error
|
||||
expect(openSpy).not.toHaveBeenCalled()
|
||||
|
||||
// Advance time past the error timeout
|
||||
vi.advanceTimersByTime(31000)
|
||||
|
||||
// Send another message
|
||||
socket.emit(SocketEventType.Enqueue, event)
|
||||
|
||||
// Now it should try to open
|
||||
expect(openSpy).toHaveBeenCalled()
|
||||
|
||||
cleanup()
|
||||
})
|
||||
})
|
||||
|
||||
describe("socketPolicyCloseOnTimeout", () => {
|
||||
it("should close socket after 30 seconds of inactivity", async () => {
|
||||
const cleanup = socketPolicyCloseOnTimeout(socket)
|
||||
const closeSpy = vi.spyOn(socket, 'close')
|
||||
|
||||
// Set socket as open
|
||||
socket.emit(SocketEventType.Status, SocketStatus.Open)
|
||||
|
||||
// Advance time past the timeout
|
||||
await vi.advanceTimersByTimeAsync(35000)
|
||||
|
||||
// Socket should be closed
|
||||
expect(closeSpy).toHaveBeenCalled()
|
||||
|
||||
cleanup()
|
||||
})
|
||||
|
||||
it("should reset timer on send activity", () => {
|
||||
const cleanup = socketPolicyCloseOnTimeout(socket)
|
||||
const closeSpy = vi.spyOn(socket, 'close')
|
||||
|
||||
// Set socket as open
|
||||
socket.emit(SocketEventType.Status, SocketStatus.Open)
|
||||
|
||||
// Advance time partially
|
||||
vi.advanceTimersByTime(20000)
|
||||
|
||||
// Send a message
|
||||
socket.emit(SocketEventType.Send, ["EVENT", { id: "123" }])
|
||||
|
||||
// Advance time partially again
|
||||
vi.advanceTimersByTime(20000)
|
||||
|
||||
// Socket should not be closed yet
|
||||
expect(closeSpy).not.toHaveBeenCalled()
|
||||
|
||||
// Advance remaining time
|
||||
vi.advanceTimersByTime(11000)
|
||||
|
||||
// Now socket should be closed
|
||||
expect(closeSpy).toHaveBeenCalled()
|
||||
|
||||
cleanup()
|
||||
})
|
||||
|
||||
it("should reset timer on receive activity", () => {
|
||||
const cleanup = socketPolicyCloseOnTimeout(socket)
|
||||
const closeSpy = vi.spyOn(socket, 'close')
|
||||
|
||||
// Set socket as open
|
||||
socket.emit(SocketEventType.Status, SocketStatus.Open)
|
||||
|
||||
// Advance time partially
|
||||
vi.advanceTimersByTime(20000)
|
||||
|
||||
// Receive a message
|
||||
socket.emit(SocketEventType.Receive, ["EVENT", "123", { id: "123" }])
|
||||
|
||||
// Advance time partially again
|
||||
vi.advanceTimersByTime(20000)
|
||||
|
||||
// Socket should not be closed yet
|
||||
expect(closeSpy).not.toHaveBeenCalled()
|
||||
|
||||
// Advance remaining time
|
||||
vi.advanceTimersByTime(11000)
|
||||
|
||||
// Now socket should be closed
|
||||
expect(closeSpy).toHaveBeenCalled()
|
||||
|
||||
cleanup()
|
||||
})
|
||||
|
||||
it("should not close socket if not open", () => {
|
||||
const cleanup = socketPolicyCloseOnTimeout(socket)
|
||||
const closeSpy = vi.spyOn(socket, 'close')
|
||||
|
||||
// Set socket as closed
|
||||
socket.emit(SocketEventType.Status, SocketStatus.Closed)
|
||||
|
||||
// Advance time past the timeout
|
||||
vi.advanceTimersByTime(31000)
|
||||
|
||||
// Socket should not be closed
|
||||
expect(closeSpy).not.toHaveBeenCalled()
|
||||
|
||||
cleanup()
|
||||
})
|
||||
})
|
||||
|
||||
describe("socketPolicyReopenActive", () => {
|
||||
it("should reopen socket when closed with pending messages", async () => {
|
||||
const cleanup = socketPolicyReopenActive(socket)
|
||||
const sendSpy = vi.spyOn(socket, 'send')
|
||||
|
||||
// Send an event that will be pending
|
||||
const event: ClientMessage = ["EVENT", { id: "123", kind: 1 }]
|
||||
socket.emit(SocketEventType.Send, event)
|
||||
|
||||
// Socket closes
|
||||
socket.emit(SocketEventType.Status, SocketStatus.Closed)
|
||||
|
||||
// Advance past the reopen delay
|
||||
await vi.advanceTimersByTimeAsync(30000)
|
||||
|
||||
// Should resend the pending event
|
||||
expect(sendSpy).toHaveBeenCalledWith(event)
|
||||
|
||||
cleanup()
|
||||
})
|
||||
|
||||
it("should reopen socket when closed with pending requests", async () => {
|
||||
const cleanup = socketPolicyReopenActive(socket)
|
||||
const sendSpy = vi.spyOn(socket, 'send')
|
||||
|
||||
// Send a request that will be pending
|
||||
const req: ClientMessage = ["REQ", "123", { kinds: [1] }]
|
||||
socket.emit(SocketEventType.Send, req)
|
||||
|
||||
// Socket closes
|
||||
socket.emit(SocketEventType.Status, SocketStatus.Closed)
|
||||
|
||||
// Advance past the reopen delay
|
||||
await vi.advanceTimersByTimeAsync(30000)
|
||||
|
||||
// Should resend the pending request
|
||||
expect(sendSpy).toHaveBeenCalledWith(req)
|
||||
|
||||
cleanup()
|
||||
})
|
||||
|
||||
it("should not reopen socket immediately after previous open", async () => {
|
||||
const cleanup = socketPolicyReopenActive(socket)
|
||||
const sendSpy = vi.spyOn(socket, 'send')
|
||||
|
||||
// Send an event that will be pending
|
||||
const event: ClientMessage = ["EVENT", { id: "123", kind: 1 }]
|
||||
socket.emit(SocketEventType.Send, event)
|
||||
|
||||
// Socket opens then closes quickly
|
||||
socket.emit(SocketEventType.Status, SocketStatus.Open)
|
||||
socket.emit(SocketEventType.Status, SocketStatus.Closed)
|
||||
|
||||
// Advance a short time
|
||||
vi.advanceTimersByTime(5000)
|
||||
|
||||
// Should not resend yet to prevent flapping
|
||||
expect(sendSpy).not.toHaveBeenCalled()
|
||||
|
||||
// Advance remaining time
|
||||
await vi.advanceTimersByTimeAsync(25000)
|
||||
|
||||
// Now should resend
|
||||
expect(sendSpy).toHaveBeenCalledWith(event)
|
||||
|
||||
cleanup()
|
||||
})
|
||||
|
||||
it("should remove pending messages when they complete", () => {
|
||||
const cleanup = socketPolicyReopenActive(socket)
|
||||
const sendSpy = vi.spyOn(socket, 'send')
|
||||
|
||||
// Send an event that will be pending
|
||||
const event: ClientMessage = ["EVENT", { id: "123", kind: 1 }]
|
||||
socket.emit(SocketEventType.Send, event)
|
||||
|
||||
// Event completes successfully
|
||||
socket.emit(SocketEventType.Receive, ["OK", "123", true])
|
||||
|
||||
// Socket closes
|
||||
socket.emit(SocketEventType.Status, SocketStatus.Closed)
|
||||
|
||||
// Advance past the reopen delay
|
||||
vi.advanceTimersByTime(30000)
|
||||
|
||||
// Should not resend since event was completed
|
||||
expect(sendSpy).not.toHaveBeenCalled()
|
||||
|
||||
cleanup()
|
||||
})
|
||||
|
||||
it("should remove pending messages when closed", () => {
|
||||
const cleanup = socketPolicyReopenActive(socket)
|
||||
const sendSpy = vi.spyOn(socket, 'send')
|
||||
|
||||
// Send a request that will be pending
|
||||
const req: ClientMessage = ["REQ", "123", { kinds: [1] }]
|
||||
socket.emit(SocketEventType.Send, req)
|
||||
|
||||
// Send close for the request
|
||||
const close: ClientMessage = ["CLOSE", "123"]
|
||||
socket.emit(SocketEventType.Send, close)
|
||||
|
||||
// Socket closes
|
||||
socket.emit(SocketEventType.Status, SocketStatus.Closed)
|
||||
|
||||
// Advance past the reopen delay
|
||||
vi.advanceTimersByTime(30000)
|
||||
|
||||
// Should not resend since request was closed
|
||||
expect(sendSpy).not.toHaveBeenCalled()
|
||||
|
||||
cleanup()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,158 @@
|
||||
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest"
|
||||
import { Nip01Signer } from '@welshman/signer'
|
||||
import { LOCAL_RELAY_URL, makeEvent } from '@welshman/util'
|
||||
import { ClientMessageType, RelayMessage } from "../src/message"
|
||||
import { AdapterContext, AbstractAdapter, AdapterEventType } from "../src/adapter"
|
||||
import { unireq, multireq, RequestEventType } from "../src/request"
|
||||
import { Tracker } from "../src/tracker"
|
||||
|
||||
class MockAdapter extends AbstractAdapter {
|
||||
constructor(readonly url: string, readonly send) {
|
||||
super()
|
||||
}
|
||||
|
||||
get sockets() {
|
||||
return []
|
||||
}
|
||||
|
||||
get urls() {
|
||||
return [this.url]
|
||||
}
|
||||
|
||||
receive = (message: RelayMessage) => {
|
||||
this.emit(AdapterEventType.Receive, message, this.url)
|
||||
}
|
||||
}
|
||||
|
||||
describe("Unireq", () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it("everything basically works", async () => {
|
||||
const sendSpy = vi.fn()
|
||||
const adapter = new MockAdapter('1', sendSpy)
|
||||
const req = unireq({
|
||||
relay: 'whatever',
|
||||
filter: {kinds: [1]},
|
||||
context: {getAdapter: () => adapter},
|
||||
autoClose: true,
|
||||
})
|
||||
|
||||
const duplicateSpy = vi.fn()
|
||||
const invalidSpy = vi.fn()
|
||||
const filteredSpy = vi.fn()
|
||||
const eventSpy = vi.fn()
|
||||
const eoseSpy = vi.fn()
|
||||
const closeSpy = vi.fn()
|
||||
|
||||
req.on(RequestEventType.Duplicate, duplicateSpy)
|
||||
req.on(RequestEventType.Invalid, invalidSpy)
|
||||
req.on(RequestEventType.Filtered, filteredSpy)
|
||||
req.on(RequestEventType.Event, eventSpy)
|
||||
req.on(RequestEventType.Eose, eoseSpy)
|
||||
req.on(RequestEventType.Close, closeSpy)
|
||||
|
||||
await vi.runAllTimers()
|
||||
|
||||
expect(sendSpy).toHaveBeenCalledWith([ClientMessageType.Req, req._id, {kinds: [1]}])
|
||||
|
||||
const signer = Nip01Signer.ephemeral()
|
||||
const event1 = await signer.sign(makeEvent(1))
|
||||
const event2 = await signer.sign(makeEvent(7))
|
||||
const event3 = makeEvent(1)
|
||||
|
||||
adapter.receive(["EVENT", req._id, event1])
|
||||
adapter.receive(["EVENT", req._id, event2])
|
||||
adapter.receive(["EVENT", req._id, event1])
|
||||
adapter.receive(["EVENT", req._id, event3])
|
||||
|
||||
await vi.runAllTimers()
|
||||
|
||||
expect(duplicateSpy).toHaveBeenCalledWith(event1)
|
||||
expect(filteredSpy).toHaveBeenCalledWith(event2)
|
||||
expect(invalidSpy).toHaveBeenCalledWith(event3)
|
||||
expect(eventSpy).toHaveBeenCalledWith(event1)
|
||||
expect(eoseSpy).toHaveBeenCalledTimes(0)
|
||||
|
||||
adapter.receive(["EOSE", req._id])
|
||||
|
||||
expect(eoseSpy).toHaveBeenCalledTimes(1)
|
||||
expect(closeSpy).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Multireq", () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it("everything basically works", async () => {
|
||||
const send1Spy = vi.fn()
|
||||
const adapter1 = new MockAdapter('1', send1Spy)
|
||||
const send2Spy = vi.fn()
|
||||
const adapter2 = new MockAdapter('2', send2Spy)
|
||||
const req = multireq({
|
||||
autoClose: true,
|
||||
relays: ['1', '2'],
|
||||
filter: {kinds: [1]},
|
||||
context: {
|
||||
getAdapter: (url: string) => url === '1' ? adapter1 : adapter2
|
||||
},
|
||||
})
|
||||
|
||||
const duplicateSpy = vi.fn()
|
||||
const invalidSpy = vi.fn()
|
||||
const filteredSpy = vi.fn()
|
||||
const eventSpy = vi.fn()
|
||||
const eoseSpy = vi.fn()
|
||||
const closeSpy = vi.fn()
|
||||
|
||||
req.on(RequestEventType.Duplicate, duplicateSpy)
|
||||
req.on(RequestEventType.Invalid, invalidSpy)
|
||||
req.on(RequestEventType.Filtered, filteredSpy)
|
||||
req.on(RequestEventType.Event, eventSpy)
|
||||
req.on(RequestEventType.Eose, eoseSpy)
|
||||
req.on(RequestEventType.Close, closeSpy)
|
||||
|
||||
await vi.runAllTimers()
|
||||
|
||||
expect(send1Spy).toHaveBeenCalledWith([ClientMessageType.Req, req._children[0]._id, {kinds: [1]}])
|
||||
expect(send2Spy).toHaveBeenCalledWith([ClientMessageType.Req, req._children[1]._id, {kinds: [1]}])
|
||||
|
||||
const signer = Nip01Signer.ephemeral()
|
||||
const event1 = await signer.sign(makeEvent(1))
|
||||
const event2 = await signer.sign(makeEvent(7))
|
||||
const event3 = makeEvent(1)
|
||||
const event4 = await signer.sign(makeEvent(1))
|
||||
|
||||
adapter1.receive(["EVENT", req._children[0]._id, event1])
|
||||
adapter1.receive(["EVENT", req._children[0]._id, event2])
|
||||
adapter1.receive(["EVENT", req._children[0]._id, event3])
|
||||
adapter2.receive(["EVENT", req._children[1]._id, event1])
|
||||
adapter2.receive(["EVENT", req._children[1]._id, event4])
|
||||
|
||||
await vi.runAllTimers()
|
||||
|
||||
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", req._children[0]._id])
|
||||
adapter2.receive(["EOSE", req._children[1]._id])
|
||||
|
||||
expect(eoseSpy).toHaveBeenCalledTimes(2)
|
||||
expect(closeSpy).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,258 +0,0 @@
|
||||
import {ctx} from "@welshman/lib"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {vi, describe, it, expect, beforeEach} from "vitest"
|
||||
import {Subscription, SubscriptionEvent} from "../../src/Subscribe"
|
||||
import {ConnectionEvent} from "../../src/ConnectionEvent"
|
||||
|
||||
describe("Subscription", () => {
|
||||
let mockExecutor: any
|
||||
let mockConnection: any
|
||||
let mockExecutorSub: any
|
||||
|
||||
const relayUrl = "wss://test.relay/"
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
mockExecutorSub = {unsubscribe: vi.fn()}
|
||||
mockConnection = {
|
||||
url: relayUrl,
|
||||
auth: {attempt: vi.fn().mockResolvedValue(undefined)},
|
||||
on: vi.fn(),
|
||||
off: vi.fn(),
|
||||
}
|
||||
mockExecutor = {
|
||||
subscribe: vi.fn().mockReturnValue(mockExecutorSub),
|
||||
target: {
|
||||
connections: [mockConnection],
|
||||
cleanup: vi.fn(),
|
||||
},
|
||||
}
|
||||
|
||||
ctx.net = {
|
||||
...ctx.net,
|
||||
getExecutor: vi.fn().mockReturnValue(mockExecutor),
|
||||
isDeleted: vi.fn().mockReturnValue(false),
|
||||
matchFilters: vi.fn().mockReturnValue(true),
|
||||
isValid: vi.fn().mockReturnValue(true),
|
||||
}
|
||||
})
|
||||
|
||||
describe("event handling", () => {
|
||||
it("should handle duplicate events", () => {
|
||||
const sub = new Subscription({
|
||||
relays: [relayUrl],
|
||||
filters: [],
|
||||
})
|
||||
const spy = vi.fn()
|
||||
sub.on(SubscriptionEvent.Duplicate, spy)
|
||||
|
||||
// Simulate duplicate event
|
||||
const event = {id: "event123"} as TrustedEvent
|
||||
sub.tracker.track(event.id, relayUrl)
|
||||
sub.onEvent(relayUrl, event)
|
||||
|
||||
expect(spy).toHaveBeenCalledWith(relayUrl, event)
|
||||
})
|
||||
|
||||
it("should handle deleted events", () => {
|
||||
const sub = new Subscription({
|
||||
relays: [relayUrl],
|
||||
filters: [],
|
||||
})
|
||||
const spy = vi.fn()
|
||||
sub.on(SubscriptionEvent.DeletedEvent, spy)
|
||||
|
||||
// @ts-ignore
|
||||
ctx.net.isDeleted.mockReturnValue(true)
|
||||
const event = {id: "event123"} as TrustedEvent
|
||||
sub.onEvent(relayUrl, event)
|
||||
|
||||
expect(spy).toHaveBeenCalledWith(relayUrl, event)
|
||||
})
|
||||
|
||||
it("should handle failed filters", () => {
|
||||
const sub = new Subscription({
|
||||
relays: [relayUrl],
|
||||
filters: [],
|
||||
})
|
||||
const spy = vi.fn()
|
||||
sub.on(SubscriptionEvent.FailedFilter, spy)
|
||||
// @ts-ignore
|
||||
ctx.net.matchFilters.mockReturnValue(false)
|
||||
const event = {id: "event123"} as TrustedEvent
|
||||
sub.onEvent(relayUrl, event)
|
||||
|
||||
expect(spy).toHaveBeenCalledWith(relayUrl, event)
|
||||
})
|
||||
|
||||
it("should handle invalid events", () => {
|
||||
const sub = new Subscription({
|
||||
relays: [relayUrl],
|
||||
filters: [],
|
||||
})
|
||||
const spy = vi.fn()
|
||||
sub.on(SubscriptionEvent.Invalid, spy)
|
||||
// @ts-ignore
|
||||
ctx.net.isValid.mockReturnValue(false)
|
||||
const event = {id: "event123"} as TrustedEvent
|
||||
sub.onEvent(relayUrl, event)
|
||||
|
||||
expect(spy).toHaveBeenCalledWith(relayUrl, event)
|
||||
})
|
||||
|
||||
it("should handle valid events", () => {
|
||||
const sub = new Subscription({
|
||||
relays: [relayUrl],
|
||||
filters: [],
|
||||
})
|
||||
const spy = vi.fn()
|
||||
sub.on(SubscriptionEvent.Event, spy)
|
||||
|
||||
const event = {id: "event123"} as TrustedEvent
|
||||
sub.onEvent(relayUrl, event)
|
||||
|
||||
expect(spy).toHaveBeenCalledWith(relayUrl, event)
|
||||
})
|
||||
})
|
||||
|
||||
describe("execution", () => {
|
||||
it("should setup auth timeout", async () => {
|
||||
const sub = new Subscription({
|
||||
relays: [relayUrl],
|
||||
filters: [{kinds: [1]}],
|
||||
authTimeout: 1000,
|
||||
})
|
||||
|
||||
await sub.execute()
|
||||
|
||||
expect(mockConnection.auth.attempt).toHaveBeenCalledWith(1000)
|
||||
})
|
||||
|
||||
it("should chunk filters", async () => {
|
||||
const filters = Array(10).fill({kinds: [1]})
|
||||
const sub = new Subscription({
|
||||
relays: [relayUrl],
|
||||
filters,
|
||||
})
|
||||
|
||||
await sub.execute()
|
||||
|
||||
expect(mockExecutor.subscribe).toHaveBeenCalledTimes(2) // 8 filters + 2 filters
|
||||
})
|
||||
|
||||
it("should handle empty filters", async () => {
|
||||
const sub = new Subscription({
|
||||
relays: [relayUrl],
|
||||
filters: [],
|
||||
})
|
||||
const spy = vi.fn()
|
||||
sub.on(SubscriptionEvent.Complete, spy)
|
||||
|
||||
await sub.execute()
|
||||
|
||||
expect(spy).toHaveBeenCalled()
|
||||
expect(mockExecutor.subscribe).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should setup connection close handlers", async () => {
|
||||
const sub = new Subscription({
|
||||
relays: [relayUrl],
|
||||
filters: [{kinds: [1]}],
|
||||
})
|
||||
|
||||
await sub.execute()
|
||||
|
||||
expect(mockConnection.on).toHaveBeenCalledWith(ConnectionEvent.Close, sub.onClose)
|
||||
})
|
||||
})
|
||||
|
||||
describe("completion", () => {
|
||||
it("should complete on timeout", async () => {
|
||||
const sub = new Subscription({
|
||||
relays: [relayUrl],
|
||||
filters: [{kinds: [1]}],
|
||||
timeout: 1000,
|
||||
})
|
||||
const spy = vi.fn()
|
||||
sub.on(SubscriptionEvent.Complete, spy)
|
||||
|
||||
await sub.execute()
|
||||
await vi.advanceTimersByTimeAsync(1000)
|
||||
|
||||
expect(spy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should complete on abort signal", async () => {
|
||||
const controller = new AbortController()
|
||||
const sub = new Subscription({
|
||||
relays: [relayUrl],
|
||||
filters: [{kinds: [1]}],
|
||||
signal: controller.signal,
|
||||
})
|
||||
const spy = vi.fn()
|
||||
sub.on(SubscriptionEvent.Complete, spy)
|
||||
|
||||
await sub.execute()
|
||||
controller.abort()
|
||||
|
||||
expect(spy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should complete when all relays close", () => {
|
||||
const sub = new Subscription({
|
||||
relays: [relayUrl],
|
||||
filters: [{kinds: [1]}],
|
||||
})
|
||||
const spy = vi.fn()
|
||||
sub.on(SubscriptionEvent.Complete, spy)
|
||||
|
||||
sub.onClose(mockConnection)
|
||||
|
||||
expect(spy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should complete on EOSE when closeOnEose is true", () => {
|
||||
const sub = new Subscription({
|
||||
relays: [relayUrl],
|
||||
filters: [{kinds: [1]}],
|
||||
closeOnEose: true,
|
||||
})
|
||||
const spy = vi.fn()
|
||||
sub.on(SubscriptionEvent.Complete, spy)
|
||||
|
||||
sub.onEose(relayUrl)
|
||||
|
||||
expect(spy).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe("cleanup", () => {
|
||||
it("should cleanup on completion", async () => {
|
||||
const sub = new Subscription({
|
||||
relays: [relayUrl],
|
||||
filters: [{kinds: [1]}],
|
||||
})
|
||||
|
||||
await sub.execute()
|
||||
sub.onComplete()
|
||||
|
||||
expect(mockExecutorSub.unsubscribe).toHaveBeenCalled()
|
||||
expect(mockExecutor.target.cleanup).toHaveBeenCalled()
|
||||
expect(mockConnection.off).toHaveBeenCalledWith(ConnectionEvent.Close, sub.onClose)
|
||||
})
|
||||
|
||||
it("should only cleanup once", async () => {
|
||||
const sub = new Subscription({
|
||||
relays: [relayUrl],
|
||||
filters: [{kinds: [1]}],
|
||||
})
|
||||
|
||||
await sub.execute()
|
||||
sub.onComplete()
|
||||
sub.onComplete()
|
||||
|
||||
expect(mockExecutorSub.unsubscribe).toHaveBeenCalledTimes(1)
|
||||
expect(mockExecutor.target.cleanup).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,173 +0,0 @@
|
||||
import {ctx} from "@welshman/lib"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {vi, describe, it, expect, beforeEach} from "vitest"
|
||||
import {
|
||||
calculateSubscriptionGroup,
|
||||
mergeSubscriptions,
|
||||
Subscription,
|
||||
SubscriptionEvent,
|
||||
} from "../../src/Subscribe"
|
||||
|
||||
describe("Subscription optimization", () => {
|
||||
let mockExecutor: any
|
||||
beforeEach(() => {
|
||||
// Setup mock executor
|
||||
mockExecutor = {
|
||||
subscribe: vi.fn().mockReturnValue({unsubscribe: vi.fn()}),
|
||||
target: {
|
||||
connections: [],
|
||||
cleanup: vi.fn(),
|
||||
},
|
||||
}
|
||||
ctx.net = {
|
||||
...ctx.net,
|
||||
optimizeSubscriptions: vi.fn(subs =>
|
||||
subs.map(sub => ({
|
||||
relays: sub.request.relays,
|
||||
filters: sub.request.filters,
|
||||
})),
|
||||
),
|
||||
getExecutor: vi.fn().mockReturnValue(mockExecutor),
|
||||
isDeleted: vi.fn().mockReturnValue(false),
|
||||
matchFilters: vi.fn().mockReturnValue(true),
|
||||
isValid: vi.fn().mockReturnValue(true),
|
||||
}
|
||||
})
|
||||
|
||||
describe("calculateSubscriptionGroup", () => {
|
||||
it("should group by timeout", () => {
|
||||
const sub = new Subscription({
|
||||
relays: ["relay1"],
|
||||
filters: [],
|
||||
timeout: 1000,
|
||||
})
|
||||
|
||||
expect(calculateSubscriptionGroup(sub)).toBe("timeout:1000")
|
||||
})
|
||||
|
||||
it("should group by auth timeout", () => {
|
||||
const sub = new Subscription({
|
||||
relays: ["relay1"],
|
||||
filters: [],
|
||||
authTimeout: 500,
|
||||
})
|
||||
|
||||
expect(calculateSubscriptionGroup(sub)).toBe("authTimeout:500")
|
||||
})
|
||||
|
||||
it("should group by closeOnEose", () => {
|
||||
const sub = new Subscription({
|
||||
relays: ["relay1"],
|
||||
filters: [],
|
||||
closeOnEose: true,
|
||||
})
|
||||
|
||||
expect(calculateSubscriptionGroup(sub)).toBe("closeOnEose")
|
||||
})
|
||||
|
||||
it("should combine multiple properties", () => {
|
||||
const sub = new Subscription({
|
||||
relays: ["relay1"],
|
||||
filters: [],
|
||||
timeout: 1000,
|
||||
authTimeout: 500,
|
||||
closeOnEose: true,
|
||||
})
|
||||
|
||||
expect(calculateSubscriptionGroup(sub)).toBe("timeout:1000|authTimeout:500|closeOnEose")
|
||||
})
|
||||
})
|
||||
|
||||
describe("mergeSubscriptions", () => {
|
||||
it("should merge relays and filters", () => {
|
||||
const subs = [
|
||||
new Subscription({
|
||||
relays: ["relay1"],
|
||||
filters: [{kinds: [1]}],
|
||||
}),
|
||||
new Subscription({
|
||||
relays: ["relay2"],
|
||||
filters: [{kinds: [2]}],
|
||||
}),
|
||||
]
|
||||
|
||||
const merged = mergeSubscriptions(subs)
|
||||
|
||||
expect(merged.request.relays).toEqual(["relay1", "relay2"])
|
||||
expect(merged.request.filters).toEqual([{kinds: [1, 2]}])
|
||||
})
|
||||
|
||||
it("should propagate events from original subscriptions to merged subscription", () => {
|
||||
const mergedSpy = vi.fn()
|
||||
const subs = [
|
||||
new Subscription({
|
||||
relays: ["relay1"],
|
||||
filters: [{kinds: [1]}],
|
||||
}),
|
||||
new Subscription({
|
||||
relays: ["relay2"],
|
||||
filters: [{kinds: [1]}],
|
||||
}),
|
||||
]
|
||||
|
||||
const merged = mergeSubscriptions(subs)
|
||||
merged.on(SubscriptionEvent.Event, mergedSpy)
|
||||
|
||||
const event = {id: "event123", kind: 1} as TrustedEvent
|
||||
|
||||
// Simulate event from original subscription
|
||||
subs[0].emit(SubscriptionEvent.Event, "relay1", event)
|
||||
|
||||
expect(mergedSpy).toHaveBeenCalledWith("relay1", event)
|
||||
})
|
||||
|
||||
it("should avoid duplicate events in merged subscription", () => {
|
||||
const mergedSpy = vi.fn()
|
||||
const subs = [
|
||||
new Subscription({
|
||||
relays: ["relay1"],
|
||||
filters: [{kinds: [1]}],
|
||||
}),
|
||||
new Subscription({
|
||||
relays: ["relay2"],
|
||||
filters: [{kinds: [1]}],
|
||||
}),
|
||||
]
|
||||
|
||||
const merged = mergeSubscriptions(subs)
|
||||
merged.on(SubscriptionEvent.Event, mergedSpy)
|
||||
|
||||
const event = {id: "event123", kind: 1} as TrustedEvent
|
||||
|
||||
// Simulate same event from both subscriptions
|
||||
subs[0].emit(SubscriptionEvent.Event, "relay1", event)
|
||||
subs[1].emit(SubscriptionEvent.Event, "relay2", event)
|
||||
|
||||
expect(mergedSpy).toHaveBeenCalledTimes(1)
|
||||
expect(mergedSpy).toHaveBeenCalledWith("relay1", event)
|
||||
})
|
||||
|
||||
it("should complete when all subscriptions complete", () => {
|
||||
const spy = vi.fn()
|
||||
const subs = [
|
||||
new Subscription({
|
||||
relays: ["relay1"],
|
||||
filters: [{kinds: [1]}],
|
||||
}),
|
||||
new Subscription({
|
||||
relays: ["relay2"],
|
||||
filters: [{kinds: [1]}],
|
||||
}),
|
||||
]
|
||||
|
||||
const merged = mergeSubscriptions(subs)
|
||||
merged.on(SubscriptionEvent.Complete, spy)
|
||||
|
||||
subs[0].emit(SubscriptionEvent.Complete)
|
||||
expect(spy).not.toHaveBeenCalled()
|
||||
|
||||
subs[1].emit(SubscriptionEvent.Complete)
|
||||
expect(spy).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,193 +0,0 @@
|
||||
import {LOCAL_RELAY_URL} from "@welshman/util"
|
||||
import {beforeEach, describe, expect, it, vi} from "vitest"
|
||||
import {ConnectionEvent, Echo, Local, Multi, Relay, Relays} from "../src/index"
|
||||
|
||||
describe("Target implementations", () => {
|
||||
describe("Echo", () => {
|
||||
it("should emit received messages", () => {
|
||||
const echo = new Echo()
|
||||
const spy = vi.fn()
|
||||
echo.on("event", spy)
|
||||
|
||||
echo.send("event", "data")
|
||||
expect(spy).toHaveBeenCalledWith("data")
|
||||
})
|
||||
|
||||
it("should cleanup properly", () => {
|
||||
const echo = new Echo()
|
||||
const spy = vi.fn()
|
||||
echo.on("event", spy)
|
||||
echo.cleanup()
|
||||
|
||||
echo.send("event", "data")
|
||||
expect(spy).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe("Local", () => {
|
||||
let mockRelay: any
|
||||
|
||||
beforeEach(() => {
|
||||
mockRelay = {
|
||||
on: vi.fn(),
|
||||
off: vi.fn(),
|
||||
send: vi.fn(),
|
||||
}
|
||||
})
|
||||
|
||||
it("should route messages through relay", async () => {
|
||||
const local = new Local(mockRelay)
|
||||
await local.send("event", "data")
|
||||
expect(mockRelay.send).toHaveBeenCalledWith("event", "data")
|
||||
})
|
||||
|
||||
it("should emit received messages with LOCAL_RELAY_URL", () => {
|
||||
const local = new Local(mockRelay)
|
||||
const spy = vi.fn()
|
||||
local.on("event", spy)
|
||||
|
||||
mockRelay.on.mock.calls[0][1]("event", "data")
|
||||
expect(spy).toHaveBeenCalledWith(LOCAL_RELAY_URL, "data")
|
||||
})
|
||||
|
||||
it("should remove relay listener on cleanup", () => {
|
||||
const local = new Local(mockRelay)
|
||||
const onMessage = mockRelay.on.mock.calls[0][1]
|
||||
|
||||
local.cleanup()
|
||||
expect(mockRelay.off).toHaveBeenCalledWith("*", onMessage)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Multi", () => {
|
||||
let target1: any
|
||||
let target2: any
|
||||
|
||||
beforeEach(() => {
|
||||
target1 = {send: vi.fn(), on: vi.fn(), cleanup: vi.fn(), connections: []}
|
||||
target2 = {send: vi.fn(), on: vi.fn(), cleanup: vi.fn(), connections: []}
|
||||
})
|
||||
|
||||
it("should forward messages to all targets", async () => {
|
||||
const multi = new Multi([target1, target2])
|
||||
await multi.send("event", "data")
|
||||
|
||||
expect(target1.send).toHaveBeenCalledWith("event", "data")
|
||||
expect(target2.send).toHaveBeenCalledWith("event", "data")
|
||||
})
|
||||
|
||||
it("should propagate events from targets", () => {
|
||||
const multi = new Multi([target1, target2])
|
||||
const spy = vi.fn()
|
||||
multi.on("event", spy)
|
||||
|
||||
target1.on.mock.calls[0][1]("event", "data")
|
||||
expect(spy).toHaveBeenCalledWith("data")
|
||||
})
|
||||
|
||||
it("should cleanup all targets", () => {
|
||||
const multi = new Multi([target1, target2])
|
||||
multi.cleanup()
|
||||
|
||||
expect(target1.cleanup).toHaveBeenCalled()
|
||||
expect(target2.cleanup).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe("Relay", () => {
|
||||
let mockConnection: any
|
||||
|
||||
beforeEach(() => {
|
||||
mockConnection = {
|
||||
on: vi.fn(),
|
||||
off: vi.fn(),
|
||||
send: vi.fn(),
|
||||
url: "test-url",
|
||||
}
|
||||
})
|
||||
|
||||
it("should forward messages to connection", async () => {
|
||||
const relay = new Relay(mockConnection)
|
||||
await relay.send("event", "data")
|
||||
expect(mockConnection.send).toHaveBeenCalledWith(["event", "data"])
|
||||
})
|
||||
|
||||
it("should emit received messages with connection url", () => {
|
||||
const relay = new Relay(mockConnection)
|
||||
const spy = vi.fn()
|
||||
relay.on("event", spy)
|
||||
|
||||
mockConnection.on.mock.calls[0][1](mockConnection, ["event", "data"])
|
||||
expect(spy).toHaveBeenCalledWith("test-url", "data")
|
||||
})
|
||||
|
||||
it("should remove connection listener on cleanup", () => {
|
||||
const relay = new Relay(mockConnection)
|
||||
const onMessage = mockConnection.on.mock.calls[0][1]
|
||||
|
||||
relay.cleanup()
|
||||
expect(mockConnection.off).toHaveBeenCalledWith(ConnectionEvent.Receive, onMessage)
|
||||
})
|
||||
|
||||
it("should stop propagating events after cleanup", () => {
|
||||
const relay = new Relay(mockConnection)
|
||||
const spy = vi.fn()
|
||||
relay.on("event", spy)
|
||||
|
||||
relay.cleanup()
|
||||
|
||||
mockConnection.on.mock.calls[0][1](mockConnection, ["event", "data"])
|
||||
expect(spy).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe("Relays", () => {
|
||||
let connections: any[]
|
||||
|
||||
beforeEach(() => {
|
||||
connections = [
|
||||
{on: vi.fn(), off: vi.fn(), send: vi.fn(), url: "url1"},
|
||||
{on: vi.fn(), off: vi.fn(), send: vi.fn(), url: "url2"},
|
||||
]
|
||||
})
|
||||
|
||||
it("should forward messages to all connections", async () => {
|
||||
const relays = new Relays(connections)
|
||||
await relays.send("event", "data")
|
||||
|
||||
connections.forEach(conn => {
|
||||
expect(conn.send).toHaveBeenCalledWith(["event", "data"])
|
||||
})
|
||||
})
|
||||
|
||||
it("should emit received messages with connection url", () => {
|
||||
const relays = new Relays(connections)
|
||||
const spy = vi.fn()
|
||||
relays.on("event", spy)
|
||||
|
||||
connections[0].on.mock.calls[0][1](connections[0], ["event", "data"])
|
||||
expect(spy).toHaveBeenCalledWith("url1", "data")
|
||||
})
|
||||
|
||||
it("should remove all connection listeners on cleanup", () => {
|
||||
const relays = new Relays(connections)
|
||||
const onMessage = connections[0].on.mock.calls[0][1] // Same handler for all connections
|
||||
|
||||
relays.cleanup()
|
||||
|
||||
connections.forEach(conn => {
|
||||
expect(conn.off).toHaveBeenCalledWith("receive:message", onMessage)
|
||||
})
|
||||
})
|
||||
|
||||
it("should stop propagating events after cleanup", () => {
|
||||
const relays = new Relays(connections)
|
||||
const spy = vi.fn()
|
||||
relays.on("event", spy)
|
||||
|
||||
relays.cleanup()
|
||||
connections[0].on.mock.calls[0][1](connections[0], ["event", "data"])
|
||||
expect(spy).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user