Compare commits
6 commits
feat/multi
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dbb2616689 | ||
|
|
34f5bcb6b7 | ||
|
|
70971c1b2a | ||
|
|
c2b60b5b3a | ||
|
|
2b004a3f77 | ||
|
|
dcda4fce6a |
13 changed files with 325 additions and 89 deletions
18
CLAUDE.md
18
CLAUDE.md
|
|
@ -21,8 +21,12 @@ This codebase communicates over a live relay with timing-sensitive handshakes an
|
||||||
**Integration tests** (`npm run test:integration` in a package):
|
**Integration tests** (`npm run test:integration` in a package):
|
||||||
- Hit the real relays at `wss://relay.riften.net:443` and `wss://relay.cauldron.quest:443`
|
- Hit the real relays at `wss://relay.riften.net:443` and `wss://relay.cauldron.quest:443`
|
||||||
- Test the full protocol handshake end-to-end
|
- Test the full protocol handshake end-to-end
|
||||||
- Located in `src/__tests__/*.integration.test.ts`
|
- Located in `src/integration/*.test.ts` (wallet is the only package with them today)
|
||||||
- Run with generous timeouts (60s per test) via `vitest.integration.config.ts`
|
- Run with generous timeouts (60s per test) via `vitest.integration.config.ts`, serially
|
||||||
|
(`singleFork`) to avoid relay contention, and with `retry: 2` — these hit third-party
|
||||||
|
relays, so a dropped connection is an environment failure rather than a regression
|
||||||
|
- A failure that reproduces locally is real; one that does not is usually the relay.
|
||||||
|
Check whether the same test passed on an earlier pipeline before assuming a regression
|
||||||
- Must pass before any release
|
- Must pass before any release
|
||||||
|
|
||||||
### Running tests
|
### Running tests
|
||||||
|
|
@ -87,3 +91,13 @@ npm run build # builds all packages in dependency order
|
||||||
```
|
```
|
||||||
|
|
||||||
Packages must be built before integration tests run (tests import from `dist/`).
|
Packages must be built before integration tests run (tests import from `dist/`).
|
||||||
|
|
||||||
|
## Releases
|
||||||
|
|
||||||
|
The `publish` CI job runs on `master` only, via `contrib/auto-publish.js`.
|
||||||
|
|
||||||
|
**The `version` in each `package.json` is a floor, not the shipped version** — all declare
|
||||||
|
`0.2.0` while npm carries higher patches. Use `npm view @wizardconnect/<pkg> version`.
|
||||||
|
|
||||||
|
The lockfile is not published, so clearing a dependency advisory means bumping the declared
|
||||||
|
range in `package.json`, not just `package-lock.json`.
|
||||||
|
|
|
||||||
|
|
@ -358,6 +358,12 @@ Either side may send a `disconnect` message before tearing down the relay connec
|
||||||
courtesy notification — the remote side treats the connection as closed immediately upon receipt
|
courtesy notification — the remote side treats the connection as closed immediately upon receipt
|
||||||
(no acknowledgement).
|
(no acknowledgement).
|
||||||
|
|
||||||
|
No acknowledgement does not mean fire-and-forget on the sender's side. `relay()` resolves only
|
||||||
|
after the publish has been settled against every configured relay, so the sender must keep the
|
||||||
|
relay connection open until then — closing it first kills the publish in flight and the peer
|
||||||
|
never learns of the disconnect. Sending a courtesy `disconnect` and immediately tearing the
|
||||||
|
transport down is the same as not sending one.
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
enum DisconnectReason {
|
enum DisconnectReason {
|
||||||
ProtocolMismatch = "protocol_mismatch", // no common protocol found during handshake
|
ProtocolMismatch = "protocol_mismatch", // no common protocol found during handshake
|
||||||
|
|
@ -373,7 +379,9 @@ interface DisconnectMessage {
|
||||||
```
|
```
|
||||||
|
|
||||||
**Wallet side** (`WalletConnectionManager`):
|
**Wallet side** (`WalletConnectionManager`):
|
||||||
- `disconnect(id)` sends `UserDisconnect` before cleaning up.
|
- `disconnect(id)` sends `UserDisconnect`, then tears the connection down once the publish
|
||||||
|
settles — bounded, so an unreachable relay cannot hold the socket open. The connection leaves
|
||||||
|
the registry synchronously. See [wallet.md § Sending disconnect](wallet.md#sending-disconnect).
|
||||||
- Incoming `disconnect` emits a `remoteDisconnect` event
|
- Incoming `disconnect` emits a `remoteDisconnect` event
|
||||||
(`connectionId`, `reason`, `message`) and removes the connection.
|
(`connectionId`, `reason`, `message`) and removes the connection.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -50,10 +50,12 @@ connect(): Promise<void>
|
||||||
// NDK connect, subscribe to GiftWrap events, start waiting for relays.
|
// NDK connect, subscribe to GiftWrap events, start waiting for relays.
|
||||||
|
|
||||||
disconnect(): Promise<void>
|
disconnect(): Promise<void>
|
||||||
// Stop subscription, mark queue not-ready, update lastProcessedTimestamp.
|
// Stop subscription, close the relay pool, mark queue not-ready, update
|
||||||
|
// lastProcessedTimestamp. Kills any in-flight publish — see below.
|
||||||
|
|
||||||
relay(message: ProtocolMessage): Promise<void>
|
relay(message: ProtocolMessage): Promise<void>
|
||||||
// Send a message. Enqueues if relays not ready. Throws if paired key not set.
|
// Send a message. Enqueues if relays not ready. Throws if paired key not set.
|
||||||
|
// Resolves only once the publish has settled against every configured relay.
|
||||||
|
|
||||||
setPairedPublicKey(key: Uint8Array): void
|
setPairedPublicKey(key: Uint8Array): void
|
||||||
// Called after key exchange. Enables outbound messages and incoming peer filtering.
|
// Called after key exchange. Enables outbound messages and incoming peer filtering.
|
||||||
|
|
@ -78,6 +80,10 @@ regardless (avoiding silent message loss on slow connections).
|
||||||
On `disconnect()`, the queue is marked not-ready so messages sent during a reconnect gap are
|
On `disconnect()`, the queue is marked not-ready so messages sent during a reconnect gap are
|
||||||
held rather than dropped.
|
held rather than dropped.
|
||||||
|
|
||||||
|
Because `disconnect()` closes the pool, it kills any publish still in flight — so anything
|
||||||
|
sending a final message before tearing down must await the `relay()` first. See
|
||||||
|
[wallet.md § Sending disconnect](wallet.md#sending-disconnect).
|
||||||
|
|
||||||
### Replay protection
|
### Replay protection
|
||||||
|
|
||||||
`lastProcessedTimestamp` is set to `now - 2` on the first connection. On reconnect it is updated
|
`lastProcessedTimestamp` is set to `now - 2` on the first connection. On reconnect it is updated
|
||||||
|
|
|
||||||
|
|
@ -69,9 +69,11 @@ class WalletConnectionManager extends EventEmitter {
|
||||||
connect(uri: string): string
|
connect(uri: string): string
|
||||||
|
|
||||||
// Tear down a specific connection, sending a UserDisconnect courtesy message.
|
// Tear down a specific connection, sending a UserDisconnect courtesy message.
|
||||||
|
// Returns as soon as the connection has left the registry; the relay socket
|
||||||
|
// closes once the courtesy message is published. See § Sending disconnect.
|
||||||
disconnect(connectionId: string): void
|
disconnect(connectionId: string): void
|
||||||
|
|
||||||
// Tear down all connections.
|
// Tear down all connections. Each is disconnected independently.
|
||||||
disconnectAll(): void
|
disconnectAll(): void
|
||||||
|
|
||||||
// Snapshot of all connections for UI rendering.
|
// Snapshot of all connections for UI rendering.
|
||||||
|
|
@ -154,6 +156,28 @@ wallet_discovered=true → set dappDiscovered=true, no further action
|
||||||
|
|
||||||
`dapp_name` and `dapp_icon` are captured from the first `dapp_ready` that includes them.
|
`dapp_name` and `dapp_icon` are captured from the first `dapp_ready` that includes them.
|
||||||
|
|
||||||
|
### Sending disconnect
|
||||||
|
|
||||||
|
`disconnect(id)` and `disconnectAll()` split teardown into two phases, because the two halves
|
||||||
|
have opposite timing requirements.
|
||||||
|
|
||||||
|
**Synchronous** — the connection is removed from the registry, its pending sign sequences are
|
||||||
|
released, and `connectionsChanged` is emitted. This cannot wait: `getConnections()` is what the
|
||||||
|
UI renders, and `connect()` returns the existing connection for a URI that already has one, so a
|
||||||
|
connection left in the map during teardown would be handed back to a caller as if it were live.
|
||||||
|
|
||||||
|
**Deferred** — the relay socket closes only after the courtesy `disconnect` message has been
|
||||||
|
published. `RelayClient.relay()` resolves after the publish settles against every configured
|
||||||
|
relay, which is a real round trip; closing the socket before that kills the publish in flight and
|
||||||
|
the dapp keeps believing the wallet is connected until its own liveness timeout fires.
|
||||||
|
|
||||||
|
The deferral is bounded by `DISCONNECT_PUBLISH_TIMEOUT_MS` (5 s). A publish that never settles is
|
||||||
|
precisely the unreachable-relay case, and a socket that is never closed is a worse failure than a
|
||||||
|
courtesy message that is never delivered.
|
||||||
|
|
||||||
|
Callers do not need to await anything. The observable contract is that state is correct
|
||||||
|
immediately and delivery is best-effort within the timeout.
|
||||||
|
|
||||||
### Receiving disconnect
|
### Receiving disconnect
|
||||||
|
|
||||||
When a `disconnect` message arrives from the dapp:
|
When a `disconnect` message arrives from the dapp:
|
||||||
|
|
|
||||||
134
package-lock.json
generated
134
package-lock.json
generated
|
|
@ -1400,15 +1400,15 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/expect": {
|
"node_modules/@vitest/expect": {
|
||||||
"version": "3.2.4",
|
"version": "3.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz",
|
||||||
"integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==",
|
"integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/chai": "^5.2.2",
|
"@types/chai": "^5.2.2",
|
||||||
"@vitest/spy": "3.2.4",
|
"@vitest/spy": "3.2.7",
|
||||||
"@vitest/utils": "3.2.4",
|
"@vitest/utils": "3.2.7",
|
||||||
"chai": "^5.2.0",
|
"chai": "^5.2.0",
|
||||||
"tinyrainbow": "^2.0.0"
|
"tinyrainbow": "^2.0.0"
|
||||||
},
|
},
|
||||||
|
|
@ -1417,13 +1417,13 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/mocker": {
|
"node_modules/@vitest/mocker": {
|
||||||
"version": "3.2.4",
|
"version": "3.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz",
|
||||||
"integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==",
|
"integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitest/spy": "3.2.4",
|
"@vitest/spy": "3.2.7",
|
||||||
"estree-walker": "^3.0.3",
|
"estree-walker": "^3.0.3",
|
||||||
"magic-string": "^0.30.17"
|
"magic-string": "^0.30.17"
|
||||||
},
|
},
|
||||||
|
|
@ -1444,9 +1444,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/pretty-format": {
|
"node_modules/@vitest/pretty-format": {
|
||||||
"version": "3.2.4",
|
"version": "3.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz",
|
||||||
"integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==",
|
"integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
@ -1457,13 +1457,13 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/runner": {
|
"node_modules/@vitest/runner": {
|
||||||
"version": "3.2.4",
|
"version": "3.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz",
|
||||||
"integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==",
|
"integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitest/utils": "3.2.4",
|
"@vitest/utils": "3.2.7",
|
||||||
"pathe": "^2.0.3",
|
"pathe": "^2.0.3",
|
||||||
"strip-literal": "^3.0.0"
|
"strip-literal": "^3.0.0"
|
||||||
},
|
},
|
||||||
|
|
@ -1472,13 +1472,13 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/snapshot": {
|
"node_modules/@vitest/snapshot": {
|
||||||
"version": "3.2.4",
|
"version": "3.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz",
|
||||||
"integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==",
|
"integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitest/pretty-format": "3.2.4",
|
"@vitest/pretty-format": "3.2.7",
|
||||||
"magic-string": "^0.30.17",
|
"magic-string": "^0.30.17",
|
||||||
"pathe": "^2.0.3"
|
"pathe": "^2.0.3"
|
||||||
},
|
},
|
||||||
|
|
@ -1487,9 +1487,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/spy": {
|
"node_modules/@vitest/spy": {
|
||||||
"version": "3.2.4",
|
"version": "3.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz",
|
||||||
"integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==",
|
"integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
@ -1500,13 +1500,13 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/utils": {
|
"node_modules/@vitest/utils": {
|
||||||
"version": "3.2.4",
|
"version": "3.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz",
|
||||||
"integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==",
|
"integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitest/pretty-format": "3.2.4",
|
"@vitest/pretty-format": "3.2.7",
|
||||||
"loupe": "^3.1.4",
|
"loupe": "^3.1.4",
|
||||||
"tinyrainbow": "^2.0.0"
|
"tinyrainbow": "^2.0.0"
|
||||||
},
|
},
|
||||||
|
|
@ -1608,16 +1608,16 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/brace-expansion": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "5.0.5",
|
"version": "5.0.9",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz",
|
||||||
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
|
"integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"balanced-match": "^4.0.2"
|
"balanced-match": "^4.0.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "18 || 20 || >=22"
|
"node": "20 || >=22"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/cac": {
|
"node_modules/cac": {
|
||||||
|
|
@ -2470,9 +2470,9 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.11",
|
"version": "3.3.18",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
|
||||||
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
"integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
|
|
@ -2706,9 +2706,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.5.12",
|
"version": "8.5.26",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz",
|
||||||
"integrity": "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==",
|
"integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
|
|
@ -2726,7 +2726,7 @@
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.11",
|
"nanoid": "^3.3.17",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
"source-map-js": "^1.2.1"
|
"source-map-js": "^1.2.1"
|
||||||
},
|
},
|
||||||
|
|
@ -3147,14 +3147,14 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "7.3.2",
|
"version": "7.3.6",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz",
|
||||||
"integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==",
|
"integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.27.0",
|
"esbuild": "^0.27.0 || ^0.28.0",
|
||||||
"fdir": "^6.5.0",
|
"fdir": "^6.5.0",
|
||||||
"picomatch": "^4.0.3",
|
"picomatch": "^4.0.3",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
|
|
@ -3246,20 +3246,20 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vitest": {
|
"node_modules/vitest": {
|
||||||
"version": "3.2.4",
|
"version": "3.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz",
|
||||||
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
|
"integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/chai": "^5.2.2",
|
"@types/chai": "^5.2.2",
|
||||||
"@vitest/expect": "3.2.4",
|
"@vitest/expect": "3.2.7",
|
||||||
"@vitest/mocker": "3.2.4",
|
"@vitest/mocker": "3.2.7",
|
||||||
"@vitest/pretty-format": "^3.2.4",
|
"@vitest/pretty-format": "^3.2.7",
|
||||||
"@vitest/runner": "3.2.4",
|
"@vitest/runner": "3.2.7",
|
||||||
"@vitest/snapshot": "3.2.4",
|
"@vitest/snapshot": "3.2.7",
|
||||||
"@vitest/spy": "3.2.4",
|
"@vitest/spy": "3.2.7",
|
||||||
"@vitest/utils": "3.2.4",
|
"@vitest/utils": "3.2.7",
|
||||||
"chai": "^5.2.0",
|
"chai": "^5.2.0",
|
||||||
"debug": "^4.4.1",
|
"debug": "^4.4.1",
|
||||||
"expect-type": "^1.2.1",
|
"expect-type": "^1.2.1",
|
||||||
|
|
@ -3289,8 +3289,8 @@
|
||||||
"@edge-runtime/vm": "*",
|
"@edge-runtime/vm": "*",
|
||||||
"@types/debug": "^4.1.12",
|
"@types/debug": "^4.1.12",
|
||||||
"@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
|
"@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
|
||||||
"@vitest/browser": "3.2.4",
|
"@vitest/browser": "3.2.7",
|
||||||
"@vitest/ui": "3.2.4",
|
"@vitest/ui": "3.2.7",
|
||||||
"happy-dom": "*",
|
"happy-dom": "*",
|
||||||
"jsdom": "*"
|
"jsdom": "*"
|
||||||
},
|
},
|
||||||
|
|
@ -3372,9 +3372,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ws": {
|
"node_modules/ws": {
|
||||||
"version": "8.19.0",
|
"version": "8.21.3",
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz",
|
||||||
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
|
"integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
@ -3408,7 +3408,7 @@
|
||||||
},
|
},
|
||||||
"packages/core": {
|
"packages/core": {
|
||||||
"name": "@wizardconnect/core",
|
"name": "@wizardconnect/core",
|
||||||
"version": "0.1.2",
|
"version": "0.2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@bch-wc2/interfaces": "^0.0.8",
|
"@bch-wc2/interfaces": "^0.0.8",
|
||||||
"@bitauth/libauth": "^3.1.0-next.2",
|
"@bitauth/libauth": "^3.1.0-next.2",
|
||||||
|
|
@ -3416,28 +3416,28 @@
|
||||||
"isomorphic-ws": "^5.0.0",
|
"isomorphic-ws": "^5.0.0",
|
||||||
"lossless-json": "^4.3.0",
|
"lossless-json": "^4.3.0",
|
||||||
"nostr-tools": "^2.23.0",
|
"nostr-tools": "^2.23.0",
|
||||||
"ws": "^8.18.0"
|
"ws": "^8.21.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5.6.2",
|
"typescript": "^5.6.2",
|
||||||
"vitest": "^3.2.3"
|
"vitest": "^3.2.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"packages/dapp": {
|
"packages/dapp": {
|
||||||
"name": "@wizardconnect/dapp",
|
"name": "@wizardconnect/dapp",
|
||||||
"version": "0.1.2",
|
"version": "0.2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wizardconnect/core": "*",
|
"@wizardconnect/core": "*",
|
||||||
"eventemitter3": "^5.0.1"
|
"eventemitter3": "^5.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5.6.2",
|
"typescript": "^5.6.2",
|
||||||
"vitest": "^3.2.3"
|
"vitest": "^3.2.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"packages/react": {
|
"packages/react": {
|
||||||
"name": "@wizardconnect/react",
|
"name": "@wizardconnect/react",
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wizardconnect/core": "*",
|
"@wizardconnect/core": "*",
|
||||||
"@wizardconnect/dapp": "*",
|
"@wizardconnect/dapp": "*",
|
||||||
|
|
@ -3450,7 +3450,7 @@
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"typescript": "^5.6.2",
|
"typescript": "^5.6.2",
|
||||||
"vitest": "^3.2.3"
|
"vitest": "^3.2.7"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": ">=18.0.0",
|
"react": ">=18.0.0",
|
||||||
|
|
@ -3511,7 +3511,7 @@
|
||||||
},
|
},
|
||||||
"packages/test-cli": {
|
"packages/test-cli": {
|
||||||
"name": "@wizardconnect/test-cli",
|
"name": "@wizardconnect/test-cli",
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@bitauth/libauth": "^3.1.0-next.2",
|
"@bitauth/libauth": "^3.1.0-next.2",
|
||||||
"@wizardconnect/core": "*",
|
"@wizardconnect/core": "*",
|
||||||
|
|
@ -3531,7 +3531,7 @@
|
||||||
},
|
},
|
||||||
"packages/wallet": {
|
"packages/wallet": {
|
||||||
"name": "@wizardconnect/wallet",
|
"name": "@wizardconnect/wallet",
|
||||||
"version": "0.1.2",
|
"version": "0.2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@bitauth/libauth": "^3.1.0-next.2",
|
"@bitauth/libauth": "^3.1.0-next.2",
|
||||||
"@wizardconnect/core": "*",
|
"@wizardconnect/core": "*",
|
||||||
|
|
@ -3539,7 +3539,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5.6.2",
|
"typescript": "^5.6.2",
|
||||||
"vitest": "^3.2.3"
|
"vitest": "^3.2.7"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,10 +40,10 @@
|
||||||
"eventemitter3": "^5.0.1",
|
"eventemitter3": "^5.0.1",
|
||||||
"isomorphic-ws": "^5.0.0",
|
"isomorphic-ws": "^5.0.0",
|
||||||
"lossless-json": "^4.3.0",
|
"lossless-json": "^4.3.0",
|
||||||
"ws": "^8.18.0"
|
"ws": "^8.21.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5.6.2",
|
"typescript": "^5.6.2",
|
||||||
"vitest": "^3.2.3"
|
"vitest": "^3.2.7"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,6 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5.6.2",
|
"typescript": "^5.6.2",
|
||||||
"vitest": "^3.2.3"
|
"vitest": "^3.2.7"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,6 @@
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"typescript": "^5.6.2",
|
"typescript": "^5.6.2",
|
||||||
"vitest": "^3.2.3"
|
"vitest": "^3.2.7"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,6 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5.6.2",
|
"typescript": "^5.6.2",
|
||||||
"vitest": "^3.2.3"
|
"vitest": "^3.2.7"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
98
packages/wallet/src/integration/disconnect-delivery.test.ts
Normal file
98
packages/wallet/src/integration/disconnect-delivery.test.ts
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
// Copyright (C) 2026 Whiterun LLC,
|
||||||
|
// This software is licensed under the GNU Lesser General Public License (LGPL), version 3.0 or later.
|
||||||
|
// A copy of the license can be found in the LICENSE file or at https://www.gnu.org/licenses/lgpl-3.0.html
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wallet-initiated disconnect must actually reach the dapp.
|
||||||
|
*
|
||||||
|
* disconnect.test.ts covers the other direction — the dapp sends `disconnect`
|
||||||
|
* and the wallet reacts. Nothing covered wallet → dapp, which is how this got
|
||||||
|
* shipped broken: doDisconnect fired the courtesy message without awaiting it and
|
||||||
|
* then tore the relay connection down, so the publish died mid-flight and the
|
||||||
|
* dapp went on believing the wallet was connected until its own liveness
|
||||||
|
* timeout. Downstream wallets were carrying a patch for it.
|
||||||
|
*
|
||||||
|
* These tests are worth their runtime because the failure is invisible locally —
|
||||||
|
* the wallet's own state is correct either way, and only the peer notices.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, afterEach } from "vitest";
|
||||||
|
import { RelayMsgAction, DisconnectReason } from "@wizardconnect/core";
|
||||||
|
import type { DisconnectMessage, ProtocolMessage } from "@wizardconnect/core";
|
||||||
|
|
||||||
|
import { setupConnection, waitFor, type ConnectionHandles } from "./helpers.js";
|
||||||
|
|
||||||
|
let handles: ConnectionHandles | null = null;
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
handles?.cleanup();
|
||||||
|
handles = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Disconnect messages the dapp actually received off the relay. */
|
||||||
|
function disconnectsSeenByDapp(h: ConnectionHandles): DisconnectMessage[] {
|
||||||
|
return h.dapp.messages.filter(
|
||||||
|
(msg: ProtocolMessage) => msg.action === RelayMsgAction.Disconnect,
|
||||||
|
) as DisconnectMessage[];
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("wallet-initiated disconnect", () => {
|
||||||
|
it("delivers the courtesy disconnect to the dapp", async () => {
|
||||||
|
handles = await setupConnection();
|
||||||
|
expect(disconnectsSeenByDapp(handles)).toHaveLength(0);
|
||||||
|
|
||||||
|
handles.wallet.manager.disconnect(handles.wallet.connectionId);
|
||||||
|
|
||||||
|
// The whole point: this arrives over a real relay, which it cannot do if the
|
||||||
|
// connection is torn down while the publish is still in flight.
|
||||||
|
await waitFor(() => disconnectsSeenByDapp(handles!).length > 0, {
|
||||||
|
timeoutMs: 20000,
|
||||||
|
what: "disconnect message received by the dapp",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(disconnectsSeenByDapp(handles)[0].reason).toBe(
|
||||||
|
DisconnectReason.UserDisconnect,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("removes the connection immediately, without waiting for delivery", async () => {
|
||||||
|
// Teardown is deferred, but the registry must not be: a caller that
|
||||||
|
// disconnects and then inspects state should never see the dying connection.
|
||||||
|
handles = await setupConnection();
|
||||||
|
const { manager, connectionId } = handles.wallet;
|
||||||
|
expect(Object.keys(manager.getConnections())).toContain(connectionId);
|
||||||
|
|
||||||
|
manager.disconnect(connectionId);
|
||||||
|
|
||||||
|
expect(Object.keys(manager.getConnections())).not.toContain(connectionId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("delivers a disconnect for every connection in disconnectAll", async () => {
|
||||||
|
handles = await setupConnection();
|
||||||
|
|
||||||
|
handles.wallet.manager.disconnectAll();
|
||||||
|
|
||||||
|
await waitFor(() => disconnectsSeenByDapp(handles!).length > 0, {
|
||||||
|
timeoutMs: 20000,
|
||||||
|
what: "disconnect message from disconnectAll",
|
||||||
|
});
|
||||||
|
expect(Object.keys(handles.wallet.manager.getConnections())).toHaveLength(
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("lets the dapp reconnect on the same URI afterwards", async () => {
|
||||||
|
// Deferring teardown must not leave the URI unusable — connect() returns an
|
||||||
|
// existing connection for a URI, so a stale entry would be handed back.
|
||||||
|
handles = await setupConnection();
|
||||||
|
const { manager, connectionId } = handles.wallet;
|
||||||
|
|
||||||
|
manager.disconnect(connectionId);
|
||||||
|
const reconnectedId = manager.connect(handles.dapp.uri);
|
||||||
|
|
||||||
|
expect(reconnectedId).not.toBe(connectionId);
|
||||||
|
expect(Object.keys(manager.getConnections())).toContain(reconnectedId);
|
||||||
|
|
||||||
|
manager.disconnect(reconnectedId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -205,10 +205,38 @@ export async function setupConnection(
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Initial dapp_ready — tells wallet we're here (wallet not yet discovered)
|
// Prompt one more wallet_ready, now that the handler above is registered.
|
||||||
|
// The wallet_ready that completed key exchange was consumed by
|
||||||
|
// initiateDappRelay before this handler existed, so it is not in
|
||||||
|
// walletReadyMessages and the caller's "wallet_ready with paths" wait
|
||||||
|
// needs a fresh one. dapp_discovered=false is what makes the wallet
|
||||||
|
// re-send.
|
||||||
await sendDappReady(false);
|
await sendDappReady(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Re-announce until key exchange completes.
|
||||||
|
//
|
||||||
|
// The wallet sends exactly one wallet_ready per connection cycle, and it
|
||||||
|
// fires as soon as manager.connect() resolves — which can be before this
|
||||||
|
// dapp's relay subscription is live. If that single message is missed there
|
||||||
|
// is nothing to retry against: keyexchangecomplete never fires, so the
|
||||||
|
// handler above never registers and no dapp_ready is ever sent. The suite
|
||||||
|
// then sat until the 15s "key exchange" timeout. Under singleFork the
|
||||||
|
// previous file's teardown is still closing sockets while this runs, which
|
||||||
|
// is exactly when the race is won by the wrong side.
|
||||||
|
//
|
||||||
|
// dapp_ready(wallet_discovered=false) resets walletReadySentThisCycle on the
|
||||||
|
// wallet, so each retry earns another wallet_ready. This is the recovery path
|
||||||
|
// the protocol's mutual-discovery design already specifies — the harness
|
||||||
|
// simply was not using it.
|
||||||
|
const reannounce = setInterval(() => {
|
||||||
|
if (keyExchanged) {
|
||||||
|
clearInterval(reannounce);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (dappClient) sendDappReady(false).catch(() => {});
|
||||||
|
}, 2000);
|
||||||
|
|
||||||
// ---- Wallet side ----
|
// ---- Wallet side ----
|
||||||
|
|
||||||
const manager = new WalletConnectionManager(adapter);
|
const manager = new WalletConnectionManager(adapter);
|
||||||
|
|
@ -216,7 +244,16 @@ export async function setupConnection(
|
||||||
|
|
||||||
// ---- Wait for key exchange ----
|
// ---- Wait for key exchange ----
|
||||||
|
|
||||||
await waitFor(() => keyExchanged, { timeoutMs: 15000, what: "key exchange" });
|
try {
|
||||||
|
await waitFor(() => keyExchanged, {
|
||||||
|
timeoutMs: 15000,
|
||||||
|
what: "key exchange",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
// Must not outlive the wait: on timeout a leaked interval keeps publishing
|
||||||
|
// dapp_ready into later tests and holds the fork open.
|
||||||
|
clearInterval(reannounce);
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Wait for wallet_ready with paths ----
|
// ---- Wait for wallet_ready with paths ----
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,15 @@ export interface PendingSignRequest {
|
||||||
request: SignTransactionRequest;
|
request: SignTransactionRequest;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How long doDisconnect waits for the courtesy `disconnect` message to be
|
||||||
|
* published before tearing the relay connection down anyway.
|
||||||
|
*
|
||||||
|
* Generous enough for a slow relay, short enough that an unreachable one cannot
|
||||||
|
* hold the socket open indefinitely.
|
||||||
|
*/
|
||||||
|
const DISCONNECT_PUBLISH_TIMEOUT_MS = 5000;
|
||||||
|
|
||||||
interface ActiveConnection {
|
interface ActiveConnection {
|
||||||
id: string;
|
id: string;
|
||||||
uri: string;
|
uri: string;
|
||||||
|
|
@ -217,22 +226,58 @@ export class WalletConnectionManager extends EventEmitter<WalletConnectionManage
|
||||||
const conn = this.connections.get(connectionId);
|
const conn = this.connections.get(connectionId);
|
||||||
if (!conn) return;
|
if (!conn) return;
|
||||||
|
|
||||||
if (sendMessage && conn.client) {
|
// Drop the connection from the registry synchronously, before any awaiting.
|
||||||
|
// getConnections() must reflect the disconnect immediately, and connect()
|
||||||
|
// returns an existing connection for a URI — so leaving this one in the map
|
||||||
|
// while its teardown is pending would hand a caller a dying connection.
|
||||||
|
for (const seq of conn.signSequences) {
|
||||||
|
this.activeSignSequences.delete(seq);
|
||||||
|
}
|
||||||
|
clearInterval(conn.notificationProcessor ?? undefined);
|
||||||
|
conn.notificationProcessor = null;
|
||||||
|
this.connections.delete(connectionId);
|
||||||
|
this.emit("connectionsChanged");
|
||||||
|
|
||||||
|
if (!sendMessage || !conn.client) {
|
||||||
|
conn.cleanup();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const disconnectMsg: DisconnectMessage = {
|
const disconnectMsg: DisconnectMessage = {
|
||||||
action: RelayMsgAction.Disconnect,
|
action: RelayMsgAction.Disconnect,
|
||||||
reason: DisconnectReason.UserDisconnect,
|
reason: DisconnectReason.UserDisconnect,
|
||||||
time: Math.floor(Date.now() / 1000),
|
time: Math.floor(Date.now() / 1000),
|
||||||
};
|
};
|
||||||
conn.client.relay(disconnectMsg).catch(() => {});
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const seq of conn.signSequences) {
|
// Tear down only once the courtesy message has actually gone out.
|
||||||
this.activeSignSequences.delete(seq);
|
//
|
||||||
}
|
// relay() resolves after `Promise.allSettled(pool.publish(...))` — a real
|
||||||
clearInterval(conn.notificationProcessor ?? undefined);
|
// round trip to every configured relay. Calling conn.cleanup() straight
|
||||||
|
// after firing it closed the pool underneath the in-flight publish, so the
|
||||||
|
// disconnect usually never reached the relay and the dapp went on believing
|
||||||
|
// the wallet was connected until its own liveness timeout fired. Downstream
|
||||||
|
// wallets were patching this out of the published package.
|
||||||
|
//
|
||||||
|
// Bounded, because "the publish never settles" is exactly the case where a
|
||||||
|
// relay is unreachable, and a socket that is never closed is worse than a
|
||||||
|
// courtesy message that is never delivered.
|
||||||
|
let torndown = false;
|
||||||
|
const teardown = () => {
|
||||||
|
if (torndown) return;
|
||||||
|
torndown = true;
|
||||||
conn.cleanup();
|
conn.cleanup();
|
||||||
this.connections.delete(connectionId);
|
};
|
||||||
this.emit("connectionsChanged");
|
|
||||||
|
const timer = setTimeout(teardown, DISCONNECT_PUBLISH_TIMEOUT_MS);
|
||||||
|
conn.client
|
||||||
|
.relay(disconnectMsg)
|
||||||
|
.catch(() => {
|
||||||
|
// Nothing to do: we are disconnecting either way.
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
teardown();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,10 @@ export default defineConfig({
|
||||||
include: ["src/integration/**/*.test.ts"],
|
include: ["src/integration/**/*.test.ts"],
|
||||||
testTimeout: 60000,
|
testTimeout: 60000,
|
||||||
hookTimeout: 60000,
|
hookTimeout: 60000,
|
||||||
|
// These tests talk to live relays. A dropped connection or a slow publish
|
||||||
|
// is an environment failure, not a regression, and without a retry a single
|
||||||
|
// one reds the whole pipeline.
|
||||||
|
retry: 2,
|
||||||
// Run integration tests serially to avoid relay contention
|
// Run integration tests serially to avoid relay contention
|
||||||
pool: "forks",
|
pool: "forks",
|
||||||
poolOptions: {
|
poolOptions: {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue