Compare commits
5 Commits
fix/durabl
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| f6e4ac8aeb | |||
| baddf5f56e | |||
| 53ce776c02 | |||
| 3c69995097 | |||
| 9789b6fc16 |
@@ -59,26 +59,40 @@ describe('Cryptographic Hardening', () => {
|
|||||||
const mismatchAtEnd = new Uint8Array(target);
|
const mismatchAtEnd = new Uint8Array(target);
|
||||||
mismatchAtEnd[len - 1] ^= 0xff;
|
mismatchAtEnd[len - 1] ^= 0xff;
|
||||||
|
|
||||||
// Measure many iterations to get a stable signal
|
const iterations = 20000;
|
||||||
const iterations = 50000;
|
const measure = (other: Uint8Array): number => {
|
||||||
|
const t0 = performance.now();
|
||||||
|
for (let i = 0; i < iterations; i++) crypto.constantTimeEqual(target, other);
|
||||||
|
return performance.now() - t0;
|
||||||
|
};
|
||||||
|
|
||||||
const start1 = performance.now();
|
// Warm up before measuring anything. The first loop through this code
|
||||||
for (let i = 0; i < iterations; i++) {
|
// pays for JIT compilation that the second one does not, which biased
|
||||||
crypto.constantTimeEqual(target, mismatchAtStart);
|
// whichever side ran first and is why the old version had to "allow 2x
|
||||||
|
// for JIT/noise" — a tolerance covering a measurement artefact rather
|
||||||
|
// than the property under test.
|
||||||
|
measure(mismatchAtStart);
|
||||||
|
measure(mismatchAtEnd);
|
||||||
|
|
||||||
|
// Paired, interleaved samples, compared by median. A single pair is one
|
||||||
|
// GC pause away from a false alarm: this test failed roughly one run in
|
||||||
|
// fourteen on a loaded machine on 08.09.2026, and a security test that
|
||||||
|
// cries wolf under load is a security test people learn to re-run until
|
||||||
|
// it passes. The median throws out the pause instead of the property.
|
||||||
|
const ratios: number[] = [];
|
||||||
|
for (let round = 0; round < 5; round++) {
|
||||||
|
const a = measure(mismatchAtStart);
|
||||||
|
const b = measure(mismatchAtEnd);
|
||||||
|
ratios.push(Math.max(a, b) / Math.min(a, b));
|
||||||
}
|
}
|
||||||
const timeStart = performance.now() - start1;
|
ratios.sort((x, y) => x - y);
|
||||||
|
const median = ratios[Math.floor(ratios.length / 2)]!;
|
||||||
|
|
||||||
const start2 = performance.now();
|
// The threshold is unchanged and deliberately so: an early-exit compare
|
||||||
for (let i = 0; i < iterations; i++) {
|
// would take ~256x longer to reach a mismatch in the last byte than the
|
||||||
crypto.constantTimeEqual(target, mismatchAtEnd);
|
// first, on every single round. Nothing about sampling weakens what this
|
||||||
}
|
// catches — it only stops the scheduler from answering for the code.
|
||||||
const timeEnd = performance.now() - start2;
|
expect(median).toBeLessThan(3);
|
||||||
|
|
||||||
// With constant-time comparison, these should be very close.
|
|
||||||
// Non-constant-time would show timeEnd >> timeStart (early exit vs full scan).
|
|
||||||
// Allow 2x variance for JIT/noise, but it should never be 10x.
|
|
||||||
const ratio = Math.max(timeStart, timeEnd) / Math.min(timeStart, timeEnd);
|
|
||||||
expect(ratio).toBeLessThan(3);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -14,8 +14,12 @@ async function startPrekeyServer(): Promise<{ url: string; stop: () => void }> {
|
|||||||
store: new MemoryPrekeyStore(),
|
store: new MemoryPrekeyStore(),
|
||||||
disableRateLimit: true,
|
disableRateLimit: true,
|
||||||
});
|
});
|
||||||
const port = 19500 + Math.floor(Math.random() * 500);
|
// Port 0 lets the OS hand out a free one. This guessed inside a 500-wide
|
||||||
const handle = Bun.serve({ port, fetch: server.fetch });
|
// range and hoped — and several files here guessed inside the SAME range,
|
||||||
|
// so with test files serving in parallel the collision arrived about a
|
||||||
|
// quarter of the time as EADDRINUSE.
|
||||||
|
const handle = Bun.serve({ port: 0, fetch: server.fetch });
|
||||||
|
const port = handle.port;
|
||||||
return { url: `http://localhost:${port}`, stop: () => handle.stop() };
|
return { url: `http://localhost:${port}`, stop: () => handle.stop() };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,8 +12,12 @@ async function startPrekeyServer(): Promise<{ url: string; stop: () => void }> {
|
|||||||
store: new MemoryPrekeyStore(),
|
store: new MemoryPrekeyStore(),
|
||||||
disableRateLimit: true,
|
disableRateLimit: true,
|
||||||
});
|
});
|
||||||
const port = 19500 + Math.floor(Math.random() * 500);
|
// Port 0 lets the OS hand out a free one. This guessed inside a 500-wide
|
||||||
const handle = Bun.serve({ port, fetch: server.fetch });
|
// range and hoped — and several files here guessed inside the SAME range,
|
||||||
|
// so with test files serving in parallel the collision arrived about a
|
||||||
|
// quarter of the time as EADDRINUSE.
|
||||||
|
const handle = Bun.serve({ port: 0, fetch: server.fetch });
|
||||||
|
const port = handle.port;
|
||||||
return { url: `http://localhost:${port}`, stop: () => handle.stop() };
|
return { url: `http://localhost:${port}`, stop: () => handle.stop() };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,8 +36,12 @@ async function startServer(): Promise<ServerHandle> {
|
|||||||
blobStore: new MemoryBlobStore(),
|
blobStore: new MemoryBlobStore(),
|
||||||
disableRateLimit: true,
|
disableRateLimit: true,
|
||||||
});
|
});
|
||||||
const port = 19000 + Math.floor(Math.random() * 500);
|
// Port 0 lets the OS hand out a free one. This guessed inside a 500-wide
|
||||||
const handle = Bun.serve({ port, fetch: app.fetch });
|
// range and hoped — and several files here guessed inside the SAME range,
|
||||||
|
// so with test files serving in parallel the collision arrived about a
|
||||||
|
// quarter of the time as EADDRINUSE.
|
||||||
|
const handle = Bun.serve({ port: 0, fetch: app.fetch });
|
||||||
|
const port = handle.port;
|
||||||
return {
|
return {
|
||||||
url: `http://localhost:${port}`,
|
url: `http://localhost:${port}`,
|
||||||
stop: () => handle.stop(true),
|
stop: () => handle.stop(true),
|
||||||
|
|||||||
@@ -25,8 +25,12 @@ async function startPrekeyServer(): Promise<{
|
|||||||
disableRateLimit: true,
|
disableRateLimit: true,
|
||||||
events,
|
events,
|
||||||
});
|
});
|
||||||
const port = 19500 + Math.floor(Math.random() * 500);
|
// Port 0 lets the OS hand out a free one. This guessed inside a 500-wide
|
||||||
const handle = Bun.serve({ port, fetch: server.fetch });
|
// range and hoped — and several files here guessed inside the SAME range,
|
||||||
|
// so with test files serving in parallel the collision arrived about a
|
||||||
|
// quarter of the time as EADDRINUSE.
|
||||||
|
const handle = Bun.serve({ port: 0, fetch: server.fetch });
|
||||||
|
const port = handle.port;
|
||||||
return {
|
return {
|
||||||
url: `http://localhost:${port}`,
|
url: `http://localhost:${port}`,
|
||||||
stop: () => handle.stop(),
|
stop: () => handle.stop(),
|
||||||
|
|||||||
@@ -26,8 +26,12 @@ async function startPrekeyServer(): Promise<{ url: string; stop: () => void }> {
|
|||||||
disableRateLimit: true,
|
disableRateLimit: true,
|
||||||
events,
|
events,
|
||||||
});
|
});
|
||||||
const port = 21000 + Math.floor(Math.random() * 500);
|
// Port 0 lets the OS hand out a free one. This guessed inside a 500-wide
|
||||||
const handle = Bun.serve({ port, fetch: server.fetch });
|
// range and hoped — and several files here guessed inside the SAME range,
|
||||||
|
// so with test files serving in parallel the collision arrived about a
|
||||||
|
// quarter of the time as EADDRINUSE.
|
||||||
|
const handle = Bun.serve({ port: 0, fetch: server.fetch });
|
||||||
|
const port = handle.port;
|
||||||
return { url: `http://localhost:${port}`, stop: () => handle.stop() };
|
return { url: `http://localhost:${port}`, stop: () => handle.stop() };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,8 +50,12 @@ async function setupRig(): Promise<TestRig> {
|
|||||||
|
|
||||||
// Spin up Bob's HTTP transfer endpoint.
|
// Spin up Bob's HTTP transfer endpoint.
|
||||||
const bobApp = await bob.transferRoute();
|
const bobApp = await bob.transferRoute();
|
||||||
const port = 21500 + Math.floor(Math.random() * 500);
|
// Port 0 lets the OS hand out a free one. This guessed inside a 500-wide
|
||||||
const bobServer = Bun.serve({ port, fetch: bobApp.fetch });
|
// range and hoped — and several files here guessed inside the SAME range,
|
||||||
|
// so with test files serving in parallel the collision arrived about a
|
||||||
|
// quarter of the time as EADDRINUSE.
|
||||||
|
const bobServer = Bun.serve({ port: 0, fetch: bobApp.fetch });
|
||||||
|
const port = bobServer.port;
|
||||||
const bobBaseUrl = `http://localhost:${port}`;
|
const bobBaseUrl = `http://localhost:${port}`;
|
||||||
|
|
||||||
// Wire up Alice's outgoing transfer routing.
|
// Wire up Alice's outgoing transfer routing.
|
||||||
|
|||||||
@@ -33,8 +33,12 @@ async function startPrekeyServer(): Promise<{ url: string; stop: () => void }> {
|
|||||||
disableRateLimit: true,
|
disableRateLimit: true,
|
||||||
events,
|
events,
|
||||||
});
|
});
|
||||||
const port = 22000 + Math.floor(Math.random() * 500);
|
// Port 0 lets the OS hand out a free one. This guessed inside a 500-wide
|
||||||
const handle = Bun.serve({ port, fetch: server.fetch });
|
// range and hoped — and several files here guessed inside the SAME range,
|
||||||
|
// so with test files serving in parallel the collision arrived about a
|
||||||
|
// quarter of the time as EADDRINUSE.
|
||||||
|
const handle = Bun.serve({ port: 0, fetch: server.fetch });
|
||||||
|
const port = handle.port;
|
||||||
return { url: `http://localhost:${port}`, stop: () => handle.stop() };
|
return { url: `http://localhost:${port}`, stop: () => handle.stop() };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,8 +53,12 @@ async function setupRig(): Promise<TestRig> {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
const bobApp = await bob.transferRoute();
|
const bobApp = await bob.transferRoute();
|
||||||
const port = 22500 + Math.floor(Math.random() * 500);
|
// Port 0 lets the OS hand out a free one. This guessed inside a 500-wide
|
||||||
const bobServer = Bun.serve({ port, fetch: bobApp.fetch });
|
// range and hoped — and several files here guessed inside the SAME range,
|
||||||
|
// so with test files serving in parallel the collision arrived about a
|
||||||
|
// quarter of the time as EADDRINUSE.
|
||||||
|
const bobServer = Bun.serve({ port: 0, fetch: bobApp.fetch });
|
||||||
|
const port = bobServer.port;
|
||||||
const bobBaseUrl = `http://localhost:${port}`;
|
const bobBaseUrl = `http://localhost:${port}`;
|
||||||
|
|
||||||
alice.configureTransfers({
|
alice.configureTransfers({
|
||||||
|
|||||||
@@ -103,8 +103,12 @@ async function startPrekeyServer(): Promise<{ url: string; stop: () => void }> {
|
|||||||
disableRateLimit: true,
|
disableRateLimit: true,
|
||||||
events,
|
events,
|
||||||
});
|
});
|
||||||
const port = 23000 + Math.floor(Math.random() * 500);
|
// Port 0 lets the OS hand out a free one. This guessed inside a 500-wide
|
||||||
const handle = Bun.serve({ port, fetch: server.fetch });
|
// range and hoped — and several files here guessed inside the SAME range,
|
||||||
|
// so with test files serving in parallel the collision arrived about a
|
||||||
|
// quarter of the time as EADDRINUSE.
|
||||||
|
const handle = Bun.serve({ port: 0, fetch: server.fetch });
|
||||||
|
const port = handle.port;
|
||||||
return { url: `http://localhost:${port}`, stop: () => handle.stop() };
|
return { url: `http://localhost:${port}`, stop: () => handle.stop() };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,13 +131,21 @@ async function setupRig(connectTimeoutMs: number): Promise<Rig> {
|
|||||||
bob.configureWebRTC({ factory: broken, connectTimeoutMs });
|
bob.configureWebRTC({ factory: broken, connectTimeoutMs });
|
||||||
|
|
||||||
const bobApp = await bob.transferRoute();
|
const bobApp = await bob.transferRoute();
|
||||||
const bobPort = 23500 + Math.floor(Math.random() * 500);
|
// Port 0 lets the OS hand out a free one. This guessed inside a 500-wide
|
||||||
const bobServer = Bun.serve({ port: bobPort, fetch: bobApp.fetch });
|
// range and hoped — and several files here guessed inside the SAME range,
|
||||||
|
// so with test files serving in parallel the collision arrived about a
|
||||||
|
// quarter of the time as EADDRINUSE.
|
||||||
|
const bobServer = Bun.serve({ port: 0, fetch: bobApp.fetch });
|
||||||
|
const bobPort = bobServer.port;
|
||||||
baseUrls.set('bob', `http://localhost:${bobPort}`);
|
baseUrls.set('bob', `http://localhost:${bobPort}`);
|
||||||
|
|
||||||
const aliceApp = await alice.transferRoute();
|
const aliceApp = await alice.transferRoute();
|
||||||
const alicePort = 24000 + Math.floor(Math.random() * 500);
|
// Port 0 lets the OS hand out a free one. This guessed inside a 500-wide
|
||||||
const aliceServer = Bun.serve({ port: alicePort, fetch: aliceApp.fetch });
|
// range and hoped — and several files here guessed inside the SAME range,
|
||||||
|
// so with test files serving in parallel the collision arrived about a
|
||||||
|
// quarter of the time as EADDRINUSE.
|
||||||
|
const aliceServer = Bun.serve({ port: 0, fetch: aliceApp.fetch });
|
||||||
|
const alicePort = aliceServer.port;
|
||||||
baseUrls.set('alice', `http://localhost:${alicePort}`);
|
baseUrls.set('alice', `http://localhost:${alicePort}`);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -145,12 +157,18 @@ async function setupRig(connectTimeoutMs: number): Promise<Rig> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function teardownRig(rig: Rig): Promise<void> {
|
async function teardownRig(rig: Rig | undefined): Promise<void> {
|
||||||
await rig.alice.shutdown();
|
// Tolerates a rig that never finished being built. When setupRig threw —
|
||||||
await rig.bob.shutdown();
|
// EADDRINUSE, before ports stopped being guessed — this ran anyway and died
|
||||||
rig.bobServerStop();
|
// on `rig.alice`, so one failure became two and the second one, a TypeError
|
||||||
rig.aliceServerStop();
|
// in teardown, is what you saw first. Cleanup must never be the loudest
|
||||||
rig.prekeyStop();
|
// thing in a failing test.
|
||||||
|
if (!rig) return;
|
||||||
|
await rig.alice?.shutdown().catch(() => {});
|
||||||
|
await rig.bob?.shutdown().catch(() => {});
|
||||||
|
try { rig.bobServerStop?.(); } catch { /* already gone */ }
|
||||||
|
try { rig.aliceServerStop?.(); } catch { /* already gone */ }
|
||||||
|
try { rig.prekeyStop?.(); } catch { /* already gone */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
function hex(b: Uint8Array): string {
|
function hex(b: Uint8Array): string {
|
||||||
|
|||||||
@@ -40,8 +40,12 @@ async function startPrekeyServer(): Promise<{ url: string; stop: () => void }> {
|
|||||||
disableRateLimit: true,
|
disableRateLimit: true,
|
||||||
events,
|
events,
|
||||||
});
|
});
|
||||||
const port = 22000 + Math.floor(Math.random() * 500);
|
// Port 0 lets the OS hand out a free one. This guessed inside a 500-wide
|
||||||
const handle = Bun.serve({ port, fetch: server.fetch });
|
// range and hoped — and several files here guessed inside the SAME range,
|
||||||
|
// so with test files serving in parallel the collision arrived about a
|
||||||
|
// quarter of the time as EADDRINUSE.
|
||||||
|
const handle = Bun.serve({ port: 0, fetch: server.fetch });
|
||||||
|
const port = handle.port;
|
||||||
return { url: `http://localhost:${port}`, stop: () => handle.stop() };
|
return { url: `http://localhost:${port}`, stop: () => handle.stop() };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,13 +74,21 @@ async function setupRig(): Promise<Rig> {
|
|||||||
bob.configureWebRTC({ factory, connectTimeoutMs: 10_000 });
|
bob.configureWebRTC({ factory, connectTimeoutMs: 10_000 });
|
||||||
|
|
||||||
const bobApp = await bob.transferRoute();
|
const bobApp = await bob.transferRoute();
|
||||||
const bobPort = 22500 + Math.floor(Math.random() * 500);
|
// Port 0 lets the OS hand out a free one. This guessed inside a 500-wide
|
||||||
const bobServer = Bun.serve({ port: bobPort, fetch: bobApp.fetch });
|
// range and hoped — and several files here guessed inside the SAME range,
|
||||||
|
// so with test files serving in parallel the collision arrived about a
|
||||||
|
// quarter of the time as EADDRINUSE.
|
||||||
|
const bobServer = Bun.serve({ port: 0, fetch: bobApp.fetch });
|
||||||
|
const bobPort = bobServer.port;
|
||||||
const bobBaseUrl = `http://localhost:${bobPort}`;
|
const bobBaseUrl = `http://localhost:${bobPort}`;
|
||||||
|
|
||||||
const aliceApp = await alice.transferRoute();
|
const aliceApp = await alice.transferRoute();
|
||||||
const alicePort = 22000 + Math.floor(Math.random() * 500);
|
// Port 0 lets the OS hand out a free one. This guessed inside a 500-wide
|
||||||
const aliceServer = Bun.serve({ port: alicePort, fetch: aliceApp.fetch });
|
// range and hoped — and several files here guessed inside the SAME range,
|
||||||
|
// so with test files serving in parallel the collision arrived about a
|
||||||
|
// quarter of the time as EADDRINUSE.
|
||||||
|
const aliceServer = Bun.serve({ port: 0, fetch: aliceApp.fetch });
|
||||||
|
const alicePort = aliceServer.port;
|
||||||
const aliceBaseUrl = `http://localhost:${alicePort}`;
|
const aliceBaseUrl = `http://localhost:${alicePort}`;
|
||||||
|
|
||||||
baseUrls.set('alice', aliceBaseUrl);
|
baseUrls.set('alice', aliceBaseUrl);
|
||||||
|
|||||||
@@ -44,8 +44,12 @@ async function startPrekeyServer(): Promise<{ url: string; stop: () => void }> {
|
|||||||
disableRateLimit: true,
|
disableRateLimit: true,
|
||||||
events,
|
events,
|
||||||
});
|
});
|
||||||
const port = 24500 + Math.floor(Math.random() * 500);
|
// Port 0 lets the OS hand out a free one. This guessed inside a 500-wide
|
||||||
const handle = Bun.serve({ port, fetch: server.fetch });
|
// range and hoped — and several files here guessed inside the SAME range,
|
||||||
|
// so with test files serving in parallel the collision arrived about a
|
||||||
|
// quarter of the time as EADDRINUSE.
|
||||||
|
const handle = Bun.serve({ port: 0, fetch: server.fetch });
|
||||||
|
const port = handle.port;
|
||||||
return { url: `http://localhost:${port}`, stop: () => handle.stop() };
|
return { url: `http://localhost:${port}`, stop: () => handle.stop() };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,13 +74,21 @@ async function setupRig(opts: { withWebRTC: boolean }): Promise<Rig> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const bobApp = await bob.transferRoute();
|
const bobApp = await bob.transferRoute();
|
||||||
const bobPort = 25000 + Math.floor(Math.random() * 500);
|
// Port 0 lets the OS hand out a free one. This guessed inside a 500-wide
|
||||||
const bobServer = Bun.serve({ port: bobPort, fetch: bobApp.fetch });
|
// range and hoped — and several files here guessed inside the SAME range,
|
||||||
|
// so with test files serving in parallel the collision arrived about a
|
||||||
|
// quarter of the time as EADDRINUSE.
|
||||||
|
const bobServer = Bun.serve({ port: 0, fetch: bobApp.fetch });
|
||||||
|
const bobPort = bobServer.port;
|
||||||
baseUrls.set('bob', `http://localhost:${bobPort}`);
|
baseUrls.set('bob', `http://localhost:${bobPort}`);
|
||||||
|
|
||||||
const aliceApp = await alice.transferRoute();
|
const aliceApp = await alice.transferRoute();
|
||||||
const alicePort = 25500 + Math.floor(Math.random() * 500);
|
// Port 0 lets the OS hand out a free one. This guessed inside a 500-wide
|
||||||
const aliceServer = Bun.serve({ port: alicePort, fetch: aliceApp.fetch });
|
// range and hoped — and several files here guessed inside the SAME range,
|
||||||
|
// so with test files serving in parallel the collision arrived about a
|
||||||
|
// quarter of the time as EADDRINUSE.
|
||||||
|
const aliceServer = Bun.serve({ port: 0, fetch: aliceApp.fetch });
|
||||||
|
const alicePort = aliceServer.port;
|
||||||
baseUrls.set('alice', `http://localhost:${alicePort}`);
|
baseUrls.set('alice', `http://localhost:${alicePort}`);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -9,10 +9,15 @@ COPY package.json bun.lock tsconfig.json ./
|
|||||||
# Copy all packages the server + observer + dashboard need
|
# Copy all packages the server + observer + dashboard need
|
||||||
COPY packages ./packages
|
COPY packages ./packages
|
||||||
|
|
||||||
RUN bun install --frozen-lockfile
|
# BuildKit gives each RUN a 1024 soft file-descriptor limit while the hard
|
||||||
|
# limit is ~1M. bun extracts tarballs in parallel, so a large package can
|
||||||
|
# exhaust the descriptors and the install dies with "Fail extracting tarball"
|
||||||
|
# — reproducible in a build, invisible outside one because `docker run`
|
||||||
|
# inherits a far higher limit. Raise the soft limit to what the host allows.
|
||||||
|
RUN ulimit -n "$(ulimit -Hn)" && bun install --frozen-lockfile
|
||||||
|
|
||||||
# Build the dashboard SPA → dist/, then copy to observer's dist/
|
# Build the dashboard SPA → dist/, then copy to observer's dist/
|
||||||
RUN cd packages/shade-dashboard && bun run build
|
RUN ulimit -n "$(ulimit -Hn)" && cd packages/shade-dashboard && bun run build
|
||||||
|
|
||||||
# ─── Production stage ───────────────────────────────────────
|
# ─── Production stage ───────────────────────────────────────
|
||||||
FROM oven/bun:1-alpine
|
FROM oven/bun:1-alpine
|
||||||
|
|||||||
@@ -451,7 +451,7 @@ export class TransferEngine {
|
|||||||
for (const q of queues.values()) q.abort(err);
|
for (const q of queues.values()) q.abort(err);
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
await Promise.all(workers);
|
await settleWorkers(workers);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async runRoundRobinUploadResumed(
|
private async runRoundRobinUploadResumed(
|
||||||
@@ -518,7 +518,7 @@ export class TransferEngine {
|
|||||||
for (const q of queues.values()) q.abort(err);
|
for (const q of queues.values()) q.abort(err);
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
await Promise.all(workers);
|
await settleWorkers(workers);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -774,7 +774,7 @@ export class TransferEngine {
|
|||||||
for (const q of queues.values()) q.abort(err);
|
for (const q of queues.values()) q.abort(err);
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
await Promise.all(workers);
|
await settleWorkers(workers);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async runRoundRobinUpload(state: OutgoingState): Promise<void> {
|
private async runRoundRobinUpload(state: OutgoingState): Promise<void> {
|
||||||
@@ -851,7 +851,7 @@ export class TransferEngine {
|
|||||||
for (const q of queues.values()) q.abort(err);
|
for (const q of queues.values()) q.abort(err);
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
await Promise.all(workers);
|
await settleWorkers(workers);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async runLaneWorker(
|
private async runLaneWorker(
|
||||||
@@ -1596,3 +1596,25 @@ function snapshotIncomingLanes(
|
|||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait for every lane worker, then report the first real failure.
|
||||||
|
*
|
||||||
|
* `Promise.all` rejects the moment one worker throws and leaves the others
|
||||||
|
* running with nobody holding their rejections — so an abort, which fails
|
||||||
|
* every lane at once, produces one propagated error and N-1 unhandled ones.
|
||||||
|
* Bun counts an unhandled rejection as a test failure, which is how the resume
|
||||||
|
* test failed roughly one run in fourteen on 08.09.2026 with a
|
||||||
|
* `TransferAbortError` the test had already caught through the handle.
|
||||||
|
*
|
||||||
|
* An abort is not the interesting failure when a real one is present, so a
|
||||||
|
* genuine error is preferred over `TransferAbortError` when both occurred.
|
||||||
|
*/
|
||||||
|
async function settleWorkers(workers: Promise<unknown>[]): Promise<void> {
|
||||||
|
const results = await Promise.allSettled(workers);
|
||||||
|
const errors = results
|
||||||
|
.filter((r): r is PromiseRejectedResult => r.status === 'rejected')
|
||||||
|
.map((r) => r.reason);
|
||||||
|
if (errors.length === 0) return;
|
||||||
|
throw errors.find((e) => !(e instanceof TransferAbortError)) ?? errors[0];
|
||||||
|
}
|
||||||
|
|||||||
@@ -41,8 +41,12 @@ describe('Resume protocol — kill-restart-verify', () => {
|
|||||||
myAddress: 'bob',
|
myAddress: 'bob',
|
||||||
});
|
});
|
||||||
const receiverApp = await createTransferRoutes(receiverEngine);
|
const receiverApp = await createTransferRoutes(receiverEngine);
|
||||||
const port = 22000 + Math.floor(Math.random() * 500);
|
// Port 0 lets the OS hand out a free one. This guessed inside a 500-wide
|
||||||
const server = Bun.serve({ port, fetch: receiverApp.fetch });
|
// range and hoped — and several files here guessed inside the SAME range,
|
||||||
|
// so with test files serving in parallel the collision arrived about a
|
||||||
|
// quarter of the time as EADDRINUSE.
|
||||||
|
const server = Bun.serve({ port: 0, fetch: receiverApp.fetch });
|
||||||
|
const port = server.port;
|
||||||
const baseUrl = `http://localhost:${port}`;
|
const baseUrl = `http://localhost:${port}`;
|
||||||
|
|
||||||
// Receiver accepts incoming.
|
// Receiver accepts incoming.
|
||||||
|
|||||||
@@ -39,8 +39,12 @@ describe('WS opt-in transport with HTTP fallback', () => {
|
|||||||
myAddress: 'bob',
|
myAddress: 'bob',
|
||||||
});
|
});
|
||||||
const httpApp = await createTransferRoutes(receiverEngine);
|
const httpApp = await createTransferRoutes(receiverEngine);
|
||||||
const httpPort = 23000 + Math.floor(Math.random() * 500);
|
// Port 0 lets the OS hand out a free one. This guessed inside a 500-wide
|
||||||
const httpServer = Bun.serve({ port: httpPort, fetch: httpApp.fetch });
|
// range and hoped — and several files here guessed inside the SAME range,
|
||||||
|
// so with test files serving in parallel the collision arrived about a
|
||||||
|
// quarter of the time as EADDRINUSE.
|
||||||
|
const httpServer = Bun.serve({ port: 0, fetch: httpApp.fetch });
|
||||||
|
const httpPort = httpServer.port;
|
||||||
const httpBaseUrl = `http://localhost:${httpPort}`;
|
const httpBaseUrl = `http://localhost:${httpPort}`;
|
||||||
cleanups.push(() => httpServer.stop());
|
cleanups.push(() => httpServer.stop());
|
||||||
|
|
||||||
|
|||||||
@@ -31,8 +31,12 @@ describe('ShadeFetchTransport with KT verifier', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const port = 20100 + Math.floor(Math.random() * 500);
|
// Port 0 lets the OS hand out a free one. This guessed inside a 500-wide
|
||||||
const handle = Bun.serve({ port, fetch: app.fetch });
|
// range and hoped — and several files here guessed inside the SAME range,
|
||||||
|
// so with test files serving in parallel the collision arrived about a
|
||||||
|
// quarter of the time as EADDRINUSE.
|
||||||
|
const handle = Bun.serve({ port: 0, fetch: app.fetch });
|
||||||
|
const port = handle.port;
|
||||||
try {
|
try {
|
||||||
const baseUrl = `http://localhost:${port}`;
|
const baseUrl = `http://localhost:${port}`;
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* Full backup mot en KJØRENDE vault — ikke Hono-fetch, men ekte HTTP.
|
* Full backup mot en KJØRENDE vault — ikke Hono-fetch, men ekte HTTP.
|
||||||
*
|
*
|
||||||
* Kjøres manuelt mot en container eller en deployet tjeneste:
|
* Kjøres manuelt mot en container eller en deployet tjeneste:
|
||||||
* SCAFFOLDD_SOCKET=... RELAY=https://vault.zyon.no bun live-e2e.ts
|
* SCAFFOLDD_SOCKET=... RELAY=https://scaffold.zyon.no bun live-e2e.ts
|
||||||
*/
|
*/
|
||||||
import { connect } from 'node:net';
|
import { connect } from 'node:net';
|
||||||
import { readFile } from 'node:fs/promises';
|
import { readFile } from 'node:fs/promises';
|
||||||
|
|||||||
Reference in New Issue
Block a user