Release 0.8.0 with replay-safe prioritized transport
This commit is contained in:
1 parent
237d404bcd
commit
009d62a155
31 files changed
+1188
-130
No files matched your search
@@ -0,0 +1,17 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {ReplayWindow} from '../dist/replay.js';
|
||||
|
||||
test('circular replay bitmap agrees with a reference across wraparound and jumps', () => {
|
||||
const window = new ReplayWindow(), seen = new Set();
|
||||
let high = -1n, seed = 17;
|
||||
const inputs = [4096n, 1n, 2n, 0n, 1n, 8191n, 4096n, 8192n];
|
||||
for (let i=0;i<15000;i++) { seed = (Math.imul(seed,1664525)+1013904223)>>>0; inputs.push(BigInt(seed % 30000)); }
|
||||
inputs.push((1n<<64n)-1n, (1n<<64n)-4096n, (1n<<64n)-4097n);
|
||||
for (const n of inputs) {
|
||||
const reject = seen.has(n) || (n <= high && high-n >= 4096n);
|
||||
assert.equal(window.containsOrStale(n), reject);
|
||||
if (!reject) { window.record(n); seen.add(n); if(n>high) high=n; assert.equal(window.containsOrStale(n),true); }
|
||||
}
|
||||
window.clear(); assert.equal(window.containsOrStale(0n),false);
|
||||
});
|
||||
@@ -25,6 +25,7 @@ async function peer(mode = 'normal') {
|
||||
return bytes.encode(encryptedPackageCodec.encode({nonce:n, encryptedMessage:session.encrypt(n,value)}));
|
||||
};
|
||||
async function receive(block) {
|
||||
if (block.kind !== 'call') return;
|
||||
if (block.name === 'L0Request') {
|
||||
const request = handshakeCodec.decode(block.packedArgs);
|
||||
session = exchange.serverSession(request.publicKey);
|
||||
@@ -47,6 +48,10 @@ async function peer(mode = 'normal') {
|
||||
const call = commandCallCodec.decode(message.payload);
|
||||
let packedResult = encrypted(protectedMessageCodec.encode({kind:'response',correlation:message.correlation,priority:message.priority,payload:blockCodec.encode({kind:'response',forId:0,packedResult:call.serializedArgs})}));
|
||||
if (mode === 'damage') packedResult[packedResult.length - 1] ^= 1;
|
||||
if (mode === 'replay-then-valid') {
|
||||
if (previous) emit({kind:'response',forId:block.id,packedResult:previous});
|
||||
previous = packedResult;
|
||||
}
|
||||
if (mode === 'replay') { const saved = packedResult; packedResult = previous ?? saved; previous = saved; }
|
||||
if (mode === 'swap') {
|
||||
calls.push({id:block.id, packedResult});
|
||||
@@ -58,8 +63,10 @@ async function peer(mode = 'normal') {
|
||||
}
|
||||
}
|
||||
return {key, socket, injectCall({id=90, signedId=90n, priority=0, signedPriority=0, kind='call'}={}) {
|
||||
emit({kind:'call', id, priority, name:'L0Call', packedArgs:encrypted(protectedMessageCodec.encode({kind,correlation:signedId,priority:signedPriority,payload:commandCallCodec.encode({name:'callback',serializedArgs:string.encode('payload')})}))});
|
||||
}, connect: (pin, extra={}) => Client.connect('ws://test.invalid/', {expectedServerKey:pin ?? key.publicKey,session:null,
|
||||
const packet = {kind:'call', id, priority, name:'L0Call', packedArgs:encrypted(protectedMessageCodec.encode({kind,correlation:signedId,priority:signedPriority,payload:commandCallCodec.encode({name:'callback',serializedArgs:string.encode('payload')})}))};
|
||||
emit(packet);
|
||||
return () => emit(packet);
|
||||
}, skipNonces(count) { nonce += BigInt(count); }, connect: (pin, extra={}) => Client.connect('ws://test.invalid/', {expectedServerKey:pin ?? key.publicKey,session:null,
|
||||
heartbeatMs:0,timeoutMs:1000,socketFactory:() => socket,...extra}), cleanup() {socket.close();session?.destroy();key.destroy();exchange.destroy(); if(peerError) throw peerError;} };
|
||||
}
|
||||
const echo = command('echo',string,string);
|
||||
@@ -86,11 +93,12 @@ test('damaged encrypted response closes connection', async () => {
|
||||
await assert.rejects(client.call(echo,'again'),e=>e.code==='closed');
|
||||
} finally {p.cleanup();}
|
||||
});
|
||||
test('replayed encrypted response closes connection', async () => {
|
||||
test('replayed encrypted response is discarded without closing connection', async () => {
|
||||
const p=await peer('replay');
|
||||
try {
|
||||
const client=await p.connect(); assert.equal(await client.call(echo,'first'),'first');
|
||||
await assert.rejects(client.call(echo,'second'),e=>e.code==='protocol'); await client.closed;
|
||||
await assert.rejects(client.call(echo,'second',{timeoutMs:20}),e=>e.code==='timeout');
|
||||
assert.equal(p.socket.readyState,1);
|
||||
} finally {p.cleanup();}
|
||||
});
|
||||
test('outer forId substitution fails closed without delivering swapped responses', async () => {
|
||||
@@ -130,3 +138,31 @@ for (const [name, metadata] of [
|
||||
} finally {p.cleanup();}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
test('a replayed response does not consume the waiter for the valid reply', async () => {
|
||||
const p = await peer('replay-then-valid');
|
||||
try {
|
||||
const client = await p.connect();
|
||||
assert.equal(await client.call(echo, 'first'), 'first');
|
||||
assert.equal(await client.call(echo, 'second'), 'second');
|
||||
assert.equal(p.socket.readyState, 1);
|
||||
} finally { p.cleanup(); }
|
||||
});
|
||||
|
||||
test('duplicate and stale incoming pushes are discarded without closing', async () => {
|
||||
const p = await peer();
|
||||
let calls = 0;
|
||||
const local = new LocalInterface().on(command('callback', string, string), () => { calls++; return 'ok'; });
|
||||
try {
|
||||
const client = await p.connect(undefined, {local});
|
||||
const replay = p.injectCall({id:0, signedId:0n, kind:'push'});
|
||||
replay();
|
||||
assert.equal(calls, 1);
|
||||
p.skipNonces(4096);
|
||||
p.injectCall({id:0, signedId:0n, kind:'push'});
|
||||
replay();
|
||||
assert.equal(calls, 2);
|
||||
assert.equal(await client.call(echo, 'still alive'), 'still alive');
|
||||
} finally { p.cleanup(); }
|
||||
});
|
||||
Reference in new issue
Block a user