99 lines
3.1 KiB
TypeScript
99 lines
3.1 KiB
TypeScript
|
|
import { describe, test, expect, afterAll } from 'bun:test';
|
||
|
|
import { SubtleCryptoProvider } from '@shade/crypto-web';
|
||
|
|
import { sha256Once } from '@shade/streams';
|
||
|
|
import {
|
||
|
|
TransferEngine,
|
||
|
|
MemoryControlChannel,
|
||
|
|
ShadeTransferHttpTransport,
|
||
|
|
ShadeTransferWsTransport,
|
||
|
|
FallbackTransferTransport,
|
||
|
|
createTransferRoutes,
|
||
|
|
type TransferHandle,
|
||
|
|
type TransferResult,
|
||
|
|
} from '../src/index.js';
|
||
|
|
|
||
|
|
const crypto = new SubtleCryptoProvider();
|
||
|
|
|
||
|
|
function hex(b: Uint8Array): string {
|
||
|
|
return Array.from(b, (x) => x.toString(16).padStart(2, '0')).join('');
|
||
|
|
}
|
||
|
|
|
||
|
|
const cleanups: Array<() => void> = [];
|
||
|
|
afterAll(() => {
|
||
|
|
for (const c of cleanups) c();
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('WS opt-in transport with HTTP fallback', () => {
|
||
|
|
test('WS connect failure → falls back to HTTP transparently', async () => {
|
||
|
|
const { a: ctrlA, b: ctrlB } = MemoryControlChannel.linked('alice', 'bob');
|
||
|
|
const receiverEngine = new TransferEngine({
|
||
|
|
crypto,
|
||
|
|
controlChannel: ctrlB,
|
||
|
|
transport: {
|
||
|
|
probe: async () => undefined,
|
||
|
|
sendChunk: async () => {
|
||
|
|
throw new Error('not used');
|
||
|
|
},
|
||
|
|
fetchResumeState: async () => null,
|
||
|
|
},
|
||
|
|
myAddress: 'bob',
|
||
|
|
});
|
||
|
|
const httpApp = await createTransferRoutes(receiverEngine);
|
||
|
|
const httpPort = 23000 + Math.floor(Math.random() * 500);
|
||
|
|
const httpServer = Bun.serve({ port: httpPort, fetch: httpApp.fetch });
|
||
|
|
const httpBaseUrl = `http://localhost:${httpPort}`;
|
||
|
|
cleanups.push(() => httpServer.stop());
|
||
|
|
|
||
|
|
const ws = new ShadeTransferWsTransport({
|
||
|
|
// Resolve to a closed port — guaranteed connect failure.
|
||
|
|
resolveWsUrl: async () => `ws://127.0.0.1:1`,
|
||
|
|
connectTimeoutMs: 500,
|
||
|
|
});
|
||
|
|
const http = new ShadeTransferHttpTransport({
|
||
|
|
resolveBaseUrl: async () => httpBaseUrl,
|
||
|
|
authenticator: {
|
||
|
|
signChunk: async () => ({ 'X-Shade-Sender-Address': 'alice' }),
|
||
|
|
signControl: async () => ({ 'X-Shade-Sender-Address': 'alice' }),
|
||
|
|
},
|
||
|
|
});
|
||
|
|
const fallback = new FallbackTransferTransport(ws, http);
|
||
|
|
|
||
|
|
const senderEngine = new TransferEngine({
|
||
|
|
crypto,
|
||
|
|
controlChannel: ctrlA,
|
||
|
|
transport: fallback,
|
||
|
|
myAddress: 'alice',
|
||
|
|
});
|
||
|
|
cleanups.push(() => senderEngine.destroy());
|
||
|
|
cleanups.push(() => receiverEngine.destroy());
|
||
|
|
|
||
|
|
let resolveRecv!: (h: TransferHandle) => void;
|
||
|
|
const recvHandlePromise = new Promise<TransferHandle>((r) => {
|
||
|
|
resolveRecv = r;
|
||
|
|
});
|
||
|
|
receiverEngine.onIncomingTransfer(async (incoming) => {
|
||
|
|
const h = await incoming.accept({ output: { kind: 'buffer' } });
|
||
|
|
resolveRecv(h);
|
||
|
|
});
|
||
|
|
|
||
|
|
const input = crypto.randomBytes(64 * 1024);
|
||
|
|
const handle = await senderEngine.upload({
|
||
|
|
to: 'bob',
|
||
|
|
input,
|
||
|
|
lanes: 2,
|
||
|
|
chunkSize: 8 * 1024,
|
||
|
|
});
|
||
|
|
const recvHandle = await recvHandlePromise;
|
||
|
|
const [senderResult, recvResult] = await Promise.all([
|
||
|
|
handle.done(),
|
||
|
|
recvHandle.done(),
|
||
|
|
]);
|
||
|
|
|
||
|
|
expect(fallback.fellBack).toBe(true);
|
||
|
|
const received =
|
||
|
|
(recvResult as TransferResult & { bytes?: Uint8Array }).bytes ?? new Uint8Array();
|
||
|
|
expect(received).toEqual(input);
|
||
|
|
expect(senderResult.sha256).toBe(hex(sha256Once(input)));
|
||
|
|
});
|
||
|
|
});
|