release(v4.6.1): bind globalThis.fetch in browser-receiver-sensitive call sites
Browsers' Window.fetch is a WebIDL bound operation; storing it as this.fetchImpl / this.fetchFn and calling via the instance receiver threw "Illegal invocation" on the first request. Bind once at construction in InboxClient, LongPollBridge, and SseBridge. Reported by Prism (multi-device E2EE terminal), blocking every browser consumer of the v4.6 transport stack on inbox.start() / bridge.connect(). WsBridge unaffected (uses WebSocket). Node/Bun fetch tolerates a free receiver, so the bug never surfaced server-side — added regression tests that install a strict-receiver globalThis.fetch to catch the issue without an actual browser harness. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
56
CHANGELOG.md
56
CHANGELOG.md
@@ -5,6 +5,62 @@ All notable changes to Shade are documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [4.6.1] — 2026-05-07 — Browser `fetch` receiver lost in `Inbox` and HTTP bridges
|
||||||
|
|
||||||
|
Every browser consumer of the v4.6.0 transport stack crashed on the
|
||||||
|
*first* network call with:
|
||||||
|
|
||||||
|
```
|
||||||
|
Failed to execute 'fetch' on 'Window': Illegal invocation
|
||||||
|
```
|
||||||
|
|
||||||
|
`@shade/inbox`, `@shade/transport-bridge` (`SseBridge`, `LongPollBridge`)
|
||||||
|
each cached the default `globalThis.fetch` reference as a class property
|
||||||
|
and later invoked it as `this.fetchImpl(url, …)` / `this.fetchFn(url, …)`.
|
||||||
|
The browser's `fetch` is a WebIDL bound operation: calling it as a
|
||||||
|
method on any object other than the `Window` rejects with the error
|
||||||
|
above. Node/Bun `fetch` tolerates a free receiver, so the bug only
|
||||||
|
manifested in actual browsers and slipped through the SDK test suite.
|
||||||
|
|
||||||
|
Reported by Prism (multi-device E2EE terminal) — `inbox.start()` →
|
||||||
|
`register()` → `client.register()` → `this.fetchImpl(url, …)` threw on
|
||||||
|
the first `/v1/inbox/register` POST, so `transport.start()` never sent
|
||||||
|
the pair handshake and the web side timed out after 30s with "PC did
|
||||||
|
not reply".
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
#### `@shade/inbox` — `InboxClient` constructor
|
||||||
|
`fetchImpl` is now `(options.fetch ?? globalThis.fetch).bind(globalThis)`.
|
||||||
|
A consumer-supplied `options.fetch` is bound too — a custom fetch with
|
||||||
|
its own receiver requirements must bind itself; binding to `globalThis`
|
||||||
|
is otherwise a no-op for free functions.
|
||||||
|
|
||||||
|
#### `@shade/transport-bridge` — `LongPollBridge` and `SseBridge` constructors
|
||||||
|
Same binding fix in both. `WsBridge` was unaffected (uses `WebSocket`).
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
- `packages/shade-inbox/tests/client.test.ts` — installs a strict-receiver
|
||||||
|
`globalThis.fetch` that mimics the WebIDL "Illegal invocation" check,
|
||||||
|
constructs `InboxClient` with no `fetch` override, runs `register()`,
|
||||||
|
and asserts the strict fetch saw `globalThis` as `this`. Pre-fix this
|
||||||
|
throws; post-fix it passes.
|
||||||
|
- `packages/shade-transport-bridge/tests/bridge.test.ts` — same regression
|
||||||
|
for both `LongPollBridge.connect()` (probe call) and `SseBridge.connect()`
|
||||||
|
(open-once call).
|
||||||
|
|
||||||
|
### Migration
|
||||||
|
|
||||||
|
None. Existing `options.fetch` overrides keep working unchanged. Apps
|
||||||
|
shipping a workaround like
|
||||||
|
|
||||||
|
```ts
|
||||||
|
new Inbox({ ..., fetch: globalThis.fetch.bind(globalThis) });
|
||||||
|
```
|
||||||
|
|
||||||
|
can drop the `.bind(globalThis)` and the redundant `fetch:` option once
|
||||||
|
they're on `4.6.1`.
|
||||||
|
|
||||||
## [4.6.0] — 2026-05-07 — Broadcast channels (Signal sender-keys for one-to-many fan-out)
|
## [4.6.0] — 2026-05-07 — Broadcast channels (Signal sender-keys for one-to-many fan-out)
|
||||||
|
|
||||||
Prism's PC desktop is the *sender* in a one-to-many fan-out — one PTY
|
Prism's PC desktop is the *sender* in a one-to-many fan-out — one PTY
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@shade/cli",
|
"name": "@shade/cli",
|
||||||
"version": "4.6.0",
|
"version": "4.6.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/cli.ts",
|
"main": "src/cli.ts",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@shade/core",
|
"name": "@shade/core",
|
||||||
"version": "4.6.0",
|
"version": "4.6.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"types": "src/index.ts",
|
"types": "src/index.ts",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@shade/crypto-web",
|
"name": "@shade/crypto-web",
|
||||||
"version": "4.6.0",
|
"version": "4.6.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"types": "src/index.ts",
|
"types": "src/index.ts",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@shade/dashboard",
|
"name": "@shade/dashboard",
|
||||||
"version": "4.6.0",
|
"version": "4.6.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@shade/files",
|
"name": "@shade/files",
|
||||||
"version": "4.6.0",
|
"version": "4.6.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"types": "src/index.ts",
|
"types": "src/index.ts",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@shade/inbox-server",
|
"name": "@shade/inbox-server",
|
||||||
"version": "4.6.0",
|
"version": "4.6.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"types": "src/index.ts",
|
"types": "src/index.ts",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@shade/inbox",
|
"name": "@shade/inbox",
|
||||||
"version": "4.6.0",
|
"version": "4.6.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"types": "src/index.ts",
|
"types": "src/index.ts",
|
||||||
|
|||||||
@@ -52,7 +52,14 @@ export class InboxClient {
|
|||||||
private readonly fetchImpl: typeof fetch;
|
private readonly fetchImpl: typeof fetch;
|
||||||
|
|
||||||
constructor(private readonly options: InboxClientOptions) {
|
constructor(private readonly options: InboxClientOptions) {
|
||||||
this.fetchImpl = options.fetch ?? globalThis.fetch;
|
// Bind once. The browser's `globalThis.fetch` is a WebIDL bound
|
||||||
|
// operation that throws "Illegal invocation" when called as a method
|
||||||
|
// on another object (which is what `this.fetchImpl(...)` does).
|
||||||
|
// Node/Bun fetch tolerates a free receiver, but binding is harmless.
|
||||||
|
// A consumer-supplied `options.fetch` is bound to the global too —
|
||||||
|
// a fetch that requires a specific receiver must bind itself.
|
||||||
|
const f = options.fetch ?? globalThis.fetch;
|
||||||
|
this.fetchImpl = f.bind(globalThis);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -281,3 +281,48 @@ describe('tamper detection', () => {
|
|||||||
expect(result.received).toBe(0);
|
expect(result.received).toBe(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('InboxClient — default fetch is bound to globalThis', () => {
|
||||||
|
// Regression: browsers' `fetch` is a WebIDL bound operation that throws
|
||||||
|
// "Illegal invocation" when called as a method on another object. The
|
||||||
|
// class stores `fetchImpl` and calls `this.fetchImpl(...)`, which strips
|
||||||
|
// the Window receiver. Constructor must `bind(globalThis)`.
|
||||||
|
test('default path passes globalThis as `this` (no Illegal invocation)', async () => {
|
||||||
|
const realFetch = globalThis.fetch;
|
||||||
|
let observedReceiver: unknown = 'unset';
|
||||||
|
function strictFetch(this: unknown, _input: unknown, _init?: unknown): Promise<Response> {
|
||||||
|
observedReceiver = this;
|
||||||
|
if (this !== globalThis) {
|
||||||
|
throw new TypeError("Failed to execute 'fetch' on 'Window': Illegal invocation");
|
||||||
|
}
|
||||||
|
return Promise.resolve(
|
||||||
|
new Response('{}', {
|
||||||
|
status: 200,
|
||||||
|
headers: { 'content-type': 'application/json' },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Object.defineProperty(globalThis, 'fetch', {
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: strictFetch,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
const id = await makeIdentity();
|
||||||
|
const client = new InboxClient({
|
||||||
|
baseUrl: 'http://example.invalid',
|
||||||
|
crypto,
|
||||||
|
signingPrivateKey: id.signingPrivateKey,
|
||||||
|
// No `fetch` override on purpose — this exercises the default path.
|
||||||
|
});
|
||||||
|
await client.register({ address: 'whoever', signingKey: id.signingPublicKey });
|
||||||
|
expect(observedReceiver).toBe(globalThis);
|
||||||
|
} finally {
|
||||||
|
Object.defineProperty(globalThis, 'fetch', {
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: realFetch,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@shade/key-transparency",
|
"name": "@shade/key-transparency",
|
||||||
"version": "4.6.0",
|
"version": "4.6.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"types": "src/index.ts",
|
"types": "src/index.ts",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@shade/keychain",
|
"name": "@shade/keychain",
|
||||||
"version": "4.6.0",
|
"version": "4.6.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"types": "src/index.ts",
|
"types": "src/index.ts",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@shade/observability",
|
"name": "@shade/observability",
|
||||||
"version": "4.6.0",
|
"version": "4.6.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"types": "src/index.ts",
|
"types": "src/index.ts",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@shade/observer",
|
"name": "@shade/observer",
|
||||||
"version": "4.6.0",
|
"version": "4.6.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"types": "src/index.ts",
|
"types": "src/index.ts",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@shade/proto",
|
"name": "@shade/proto",
|
||||||
"version": "4.6.0",
|
"version": "4.6.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"types": "src/index.ts",
|
"types": "src/index.ts",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@shade/recovery",
|
"name": "@shade/recovery",
|
||||||
"version": "4.6.0",
|
"version": "4.6.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"types": "src/index.ts",
|
"types": "src/index.ts",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@shade/sdk",
|
"name": "@shade/sdk",
|
||||||
"version": "4.6.0",
|
"version": "4.6.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"types": "src/index.ts",
|
"types": "src/index.ts",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@shade/server",
|
"name": "@shade/server",
|
||||||
"version": "4.6.0",
|
"version": "4.6.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"types": "src/index.ts",
|
"types": "src/index.ts",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@shade/storage-encrypted",
|
"name": "@shade/storage-encrypted",
|
||||||
"version": "4.6.0",
|
"version": "4.6.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"types": "src/index.ts",
|
"types": "src/index.ts",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@shade/storage-indexeddb",
|
"name": "@shade/storage-indexeddb",
|
||||||
"version": "4.6.0",
|
"version": "4.6.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"types": "src/index.ts",
|
"types": "src/index.ts",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@shade/storage-postgres",
|
"name": "@shade/storage-postgres",
|
||||||
"version": "4.6.0",
|
"version": "4.6.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"types": "src/index.ts",
|
"types": "src/index.ts",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@shade/storage-sqlite",
|
"name": "@shade/storage-sqlite",
|
||||||
"version": "4.6.0",
|
"version": "4.6.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"types": "src/index.ts",
|
"types": "src/index.ts",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@shade/streams",
|
"name": "@shade/streams",
|
||||||
"version": "4.6.0",
|
"version": "4.6.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"types": "src/index.ts",
|
"types": "src/index.ts",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@shade/transfer",
|
"name": "@shade/transfer",
|
||||||
"version": "4.6.0",
|
"version": "4.6.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"types": "src/index.ts",
|
"types": "src/index.ts",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@shade/transport-bridge",
|
"name": "@shade/transport-bridge",
|
||||||
"version": "4.6.0",
|
"version": "4.6.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"types": "src/index.ts",
|
"types": "src/index.ts",
|
||||||
|
|||||||
@@ -51,7 +51,10 @@ export class LongPollBridge implements BridgeTransport {
|
|||||||
private loopPromise: Promise<void> | null = null;
|
private loopPromise: Promise<void> | null = null;
|
||||||
|
|
||||||
constructor(private readonly options: LongPollBridgeOptions) {
|
constructor(private readonly options: LongPollBridgeOptions) {
|
||||||
this.fetchFn = options.fetch ?? globalThis.fetch;
|
// Bind to globalThis: browser `fetch` is a WebIDL bound operation
|
||||||
|
// and throws "Illegal invocation" when called as `this.fetchFn(...)`.
|
||||||
|
const f = options.fetch ?? globalThis.fetch;
|
||||||
|
this.fetchFn = f.bind(globalThis);
|
||||||
this.cursor = options.startCursor ?? 0;
|
this.cursor = options.startCursor ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -58,7 +58,10 @@ export class SseBridge implements BridgeTransport {
|
|||||||
console.warn('[shade-bridge:sse]', err.message);
|
console.warn('[shade-bridge:sse]', err.message);
|
||||||
|
|
||||||
constructor(private readonly options: SseBridgeOptions) {
|
constructor(private readonly options: SseBridgeOptions) {
|
||||||
this.fetchFn = options.fetch ?? globalThis.fetch;
|
// Bind to globalThis: browser `fetch` is a WebIDL bound operation
|
||||||
|
// and throws "Illegal invocation" when called as `this.fetchFn(...)`.
|
||||||
|
const f = options.fetch ?? globalThis.fetch;
|
||||||
|
this.fetchFn = f.bind(globalThis);
|
||||||
this.cursor = options.startCursor ?? 0;
|
this.cursor = options.startCursor ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -510,3 +510,104 @@ describe('Bridge cursor resume', () => {
|
|||||||
await sseB.disconnect();
|
await sseB.disconnect();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Bridges — default fetch is bound to globalThis', () => {
|
||||||
|
// Regression: browsers' `fetch` is a WebIDL bound operation that throws
|
||||||
|
// "Illegal invocation" when called via `this.fetchFn(...)`. Constructors
|
||||||
|
// for LongPollBridge / SseBridge must `bind(globalThis)`.
|
||||||
|
function installStrictFetch(): { restore: () => void; getReceiver: () => unknown } {
|
||||||
|
const realFetch = globalThis.fetch;
|
||||||
|
let observedReceiver: unknown = 'unset';
|
||||||
|
function strictFetch(this: unknown, _input: unknown, _init?: unknown): Promise<Response> {
|
||||||
|
observedReceiver = this;
|
||||||
|
if (this !== globalThis) {
|
||||||
|
throw new TypeError("Failed to execute 'fetch' on 'Window': Illegal invocation");
|
||||||
|
}
|
||||||
|
return Promise.resolve(
|
||||||
|
new Response('{"blobs":[]}', {
|
||||||
|
status: 200,
|
||||||
|
headers: { 'content-type': 'application/json' },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Object.defineProperty(globalThis, 'fetch', {
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: strictFetch,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
restore: () =>
|
||||||
|
Object.defineProperty(globalThis, 'fetch', {
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: realFetch,
|
||||||
|
}),
|
||||||
|
getReceiver: () => observedReceiver,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test('LongPollBridge default path passes globalThis as `this`', async () => {
|
||||||
|
const { restore, getReceiver } = installStrictFetch();
|
||||||
|
try {
|
||||||
|
const id = await generateIdentityKeyPair(crypto);
|
||||||
|
const bridge = new LongPollBridge({
|
||||||
|
baseUrl: 'http://example.invalid',
|
||||||
|
auth: { crypto, signingPrivateKey: id.signingPrivateKey, address: 'foo' },
|
||||||
|
pollTimeoutMs: 100,
|
||||||
|
requestTimeoutMs: 200,
|
||||||
|
errorBackoffMs: 50,
|
||||||
|
disableLoop: true,
|
||||||
|
});
|
||||||
|
await bridge.connect({ onMessage: () => {} });
|
||||||
|
expect(getReceiver()).toBe(globalThis);
|
||||||
|
await bridge.disconnect();
|
||||||
|
} finally {
|
||||||
|
restore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('SseBridge default path passes globalThis as `this`', async () => {
|
||||||
|
// For SSE we only need to know the very first fetch was bound; the
|
||||||
|
// 200-with-empty-stream response will let openOnce return cleanly,
|
||||||
|
// and disableAutoReconnect prevents an infinite reconnect loop.
|
||||||
|
const realFetch = globalThis.fetch;
|
||||||
|
let observedReceiver: unknown = 'unset';
|
||||||
|
function strictFetch(this: unknown, _input: unknown, _init?: unknown): Promise<Response> {
|
||||||
|
observedReceiver = this;
|
||||||
|
if (this !== globalThis) {
|
||||||
|
throw new TypeError("Failed to execute 'fetch' on 'Window': Illegal invocation");
|
||||||
|
}
|
||||||
|
// Empty SSE-shaped body: stream closes immediately.
|
||||||
|
return Promise.resolve(
|
||||||
|
new Response('', {
|
||||||
|
status: 200,
|
||||||
|
headers: { 'content-type': 'text/event-stream' },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Object.defineProperty(globalThis, 'fetch', {
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: strictFetch,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
const id = await generateIdentityKeyPair(crypto);
|
||||||
|
const bridge = new SseBridge({
|
||||||
|
baseUrl: 'http://example.invalid',
|
||||||
|
auth: { crypto, signingPrivateKey: id.signingPrivateKey, address: 'foo' },
|
||||||
|
initialBackoffMs: 50,
|
||||||
|
maxBackoffMs: 100,
|
||||||
|
disableAutoReconnect: true,
|
||||||
|
});
|
||||||
|
await bridge.connect({ onMessage: () => {} });
|
||||||
|
expect(observedReceiver).toBe(globalThis);
|
||||||
|
await bridge.disconnect();
|
||||||
|
} finally {
|
||||||
|
Object.defineProperty(globalThis, 'fetch', {
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: realFetch,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@shade/transport-webrtc",
|
"name": "@shade/transport-webrtc",
|
||||||
"version": "4.6.0",
|
"version": "4.6.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"types": "src/index.ts",
|
"types": "src/index.ts",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@shade/transport",
|
"name": "@shade/transport",
|
||||||
"version": "4.6.0",
|
"version": "4.6.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"types": "src/index.ts",
|
"types": "src/index.ts",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@shade/widgets",
|
"name": "@shade/widgets",
|
||||||
"version": "4.6.0",
|
"version": "4.6.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"types": "src/index.ts",
|
"types": "src/index.ts",
|
||||||
|
|||||||
Reference in New Issue
Block a user