"use strict"; const assert = require("node:assert/strict"); const test = require("node:test"); const { PluginStreamRouter } = require("./streamRouter.cjs"); test("incoming stream credit is returned only after the consumer releases a chunk", async () => { const sent = []; const received = []; const router = new PluginStreamRouter({ send(message) { sent.push(message); }, onIncomingStream({ bind }) { bind({ async onChunk(chunk, release) { received.push(chunk.value); assert.equal(sent.length, 0); release(); }, }); return true; }, }); await router.accept({ frame: { streamId: "input", sequence: 0, kind: "open", windowBytes: 1024 } }); const contract = await import("@netcatty/plugin-contract"); await router.accept({ frame: { streamId: "input", sequence: 1, kind: "chunk", data: contract.createJsonStreamChunk({ value: 1 }), }, }); assert.deepEqual(received, [{ value: 1 }]); assert.deepEqual(sent[0].frame, { streamId: "input", sequence: 0, kind: "windowUpdate", creditBytes: contract.createJsonStreamChunk({ value: 1 }).byteLength, }); }); test("incoming binary streams return their declared byte credit after release", async () => { const sent = []; const received = []; const router = new PluginStreamRouter({ send(message) { sent.push(message); }, onIncomingStream({ bind }) { bind({ onChunk(chunk, release) { received.push([...chunk.bytes]); release(); }, }); return true; }, }); const contract = await import("@netcatty/plugin-contract"); const bytes = new Uint8Array([1, 2, 3, 4]); await router.accept({ frame: { streamId: "binary", sequence: 0, kind: "open", windowBytes: 1024 } }); await router.accept({ frame: { streamId: "binary", sequence: 1, kind: "chunk", data: contract.createBase64StreamChunk(bytes), }, }); assert.deepEqual(received, [[1, 2, 3, 4]]); assert.deepEqual(sent[0].frame, { streamId: "binary", sequence: 0, kind: "windowUpdate", creditBytes: 4, }); assert.equal(Number.isSafeInteger(sent[0].frame.creditBytes), true); }); test("outgoing stream applies byte backpressure and ordered credit updates", async () => { const sent = []; const router = new PluginStreamRouter({ send(message) { sent.push(message); } }); const stream = await router.openOutgoing("output", 1024); const source = new Uint8Array(1024); source[0] = 7; await stream.write(source); source[0] = 99; assert.equal(new Uint8Array(sent[1].transfer)[0], 7); let secondResolved = false; const second = stream.write(new Uint8Array(1)).then(() => { secondResolved = true; }); await new Promise((resolve) => setImmediate(resolve)); assert.equal(secondResolved, false); await router.accept({ frame: { streamId: "output", sequence: 0, kind: "windowUpdate", creditBytes: 1 }, }); await second; assert.equal(sent.filter((message) => message.frame.kind === "chunk").length, 2); await assert.rejects( router.accept({ frame: { streamId: "output", sequence: 0, kind: "windowUpdate", creditBytes: 1 } }), /Out-of-order/, ); await assert.rejects( router.accept({ frame: { streamId: "output", sequence: 2, kind: "windowUpdate", creditBytes: 1 } }), /Out-of-order/, ); }); test("outgoing JSON stream chunks preserve the exact contract shape", async () => { const sent = []; const router = new PluginStreamRouter({ send(message) { sent.push(message); } }); const stream = await router.openOutgoing("json-output", 1024); const value = { nested: ["value", 1] }; const contract = await import("@netcatty/plugin-contract"); await stream.write(value); assert.deepEqual(sent[1].frame.data, contract.createJsonStreamChunk(value)); assert.equal(Object.hasOwn(sent[1].frame.data, "transfer"), false); assert.equal(Object.hasOwn(sent[1], "transfer"), false); }); test("ended outgoing streams accept final receive credit before retiring", async () => { const sent = []; const router = new PluginStreamRouter({ send(message) { sent.push(message); } }); const stream = await router.openOutgoing("ended-output", 1024); const contract = await import("@netcatty/plugin-contract"); const chunk = contract.createJsonStreamChunk({ final: true }); await stream.write({ final: true }); stream.end(); assert.equal(sent.at(-1).frame.kind, "end"); assert.equal(router.outgoing.has("ended-output"), true); await assert.rejects(stream.write({ late: true }), /closed/); await router.accept({ frame: { streamId: "ended-output", sequence: 0, kind: "windowUpdate", creditBytes: chunk.byteLength, }, }); assert.equal(router.outgoing.has("ended-output"), false); }); test("unhandled incoming streams are cancelled immediately", async () => { const sent = []; const router = new PluginStreamRouter({ send(message) { sent.push(message); } }); await router.accept({ frame: { streamId: "unhandled", sequence: 0, kind: "open", windowBytes: 1024 } }); assert.equal(sent[0].frame.kind, "cancel"); }); test("a stalled incoming owner is aborted and cannot block later streams", async () => { const sent = []; const signals = []; const closed = []; let calls = 0; const router = new PluginStreamRouter({ openTimeoutMs: 10, send(message) { sent.push(message); }, onIncomingStream({ bind, signal }) { calls += 1; signals.push(signal); if (calls === 1) { bind({ onChunk() {}, onClose(reason) { closed.push(reason); } }); return new Promise(() => {}); } bind({ onChunk() {} }); return true; }, }); await router.accept({ frame: { streamId: "stalled", sequence: 0, kind: "open", windowBytes: 1024 }, }); assert.equal(signals[0].aborted, true); assert.equal(closed[0], signals[0].reason); assert.equal(sent[0].frame.kind, "cancel"); assert.equal(router.incoming.has("stalled"), false); await router.accept({ frame: { streamId: "next", sequence: 0, kind: "open", windowBytes: 1024 }, }); assert.equal(router.incoming.has("next"), true); }); test("incoming streams require an explicit accept and a chunk handler", async () => { const sent = []; const router = new PluginStreamRouter({ send(message) { sent.push(message); }, onIncomingStream() {}, }); await router.accept({ frame: { streamId: "implicit", sequence: 0, kind: "open", windowBytes: 1024 } }); assert.equal(sent[0].frame.kind, "cancel"); const invalid = new PluginStreamRouter({ send() {}, onIncomingStream({ bind }) { bind({}); return true; }, }); await assert.rejects( invalid.accept({ frame: { streamId: "invalid", sequence: 0, kind: "open", windowBytes: 1024 } }), /onChunk handler is required/, ); assert.equal(invalid.incoming.size, 0); }); test("owner-selection failures close an already-bound incoming stream", async () => { const sent = []; const closed = []; const router = new PluginStreamRouter({ send(message) { sent.push(message); }, onIncomingStream({ bind }) { bind({ onChunk() {}, onClose(reason) { closed.push(reason); } }); throw new Error("owner setup failed"); }, }); await assert.rejects( router.accept({ frame: { streamId: "failed-owner", sequence: 0, kind: "open", windowBytes: 1024 } }), /owner setup failed/, ); assert.equal(sent[0].frame.kind, "cancel"); assert.match(closed[0].message, /owner setup failed/); assert.equal(router.incoming.size, 0); }); test("stream envelopes reject extra fields and accessors before state changes", async () => { const router = new PluginStreamRouter({ send() {} }); await assert.rejects(router.accept({ frame: { streamId: "extra", sequence: 0, kind: "open", windowBytes: 1024 }, unexpected: true, }), /unknown properties/); const accessor = {}; Object.defineProperty(accessor, "frame", { enumerable: true, get() { throw new Error("accessor invoked"); }, }); await assert.rejects(router.accept(accessor), /data properties/); }); test("peer cancellation closes only the matching outgoing stream", async () => { const sent = []; const router = new PluginStreamRouter({ send(message) { sent.push(message); } }); const stream = await router.openOutgoing("cancelled", 1024); await router.accept({ frame: { streamId: "cancelled", sequence: 1, kind: "cancel" }, }); await assert.rejects(stream.write(new Uint8Array([1])), /closed/); const other = await router.openOutgoing("other", 1024); await other.write(new Uint8Array([2])); assert.equal(sent.at(-1).frame.streamId, "other"); }); test("transport failure while opening a stream releases its identity", async () => { const router = new PluginStreamRouter({ send() { throw new Error("transport closed"); }, }); await assert.rejects(router.openOutgoing("failed-open", 1024), /transport closed/); assert.equal(router.outgoing.size, 0); }); test("transport failure while returning credit closes the incoming stream", async () => { const closed = []; let releaseChunk; const router = new PluginStreamRouter({ send() { throw new Error("transport closed"); }, onIncomingStream({ bind }) { bind({ onChunk(_chunk, release) { releaseChunk = release; }, onClose(reason) { closed.push(reason); }, }); return true; }, }); const contract = await import("@netcatty/plugin-contract"); await router.accept({ frame: { streamId: "failed-credit", sequence: 0, kind: "open", windowBytes: 1024 } }); await router.accept({ frame: { streamId: "failed-credit", sequence: 1, kind: "chunk", data: contract.createJsonStreamChunk({ value: 1 }), }, }); assert.throws(() => releaseChunk(), /transport closed/); assert.equal(router.incoming.size, 0); assert.equal(closed.length, 1); assert.match(closed[0].message, /transport closed/); }); test("incoming stream completion waits for asynchronous owner cleanup", async () => { const sent = []; let ownerSignal; let releaseCleanup; const cleanup = new Promise((resolve) => { releaseCleanup = resolve; }); let cleanupFinished = false; const router = new PluginStreamRouter({ send(message) { sent.push(message); }, onIncomingStream(stream) { ownerSignal = stream.signal; stream.bind({ onChunk() {}, async onClose() { await cleanup; cleanupFinished = true; }, }); return true; }, }); await router.accept({ frame: { streamId: "cleanup", sequence: 0, kind: "open", windowBytes: 1_024 } }); let settled = false; const ending = router.accept({ frame: { streamId: "cleanup", sequence: 1, kind: "end" } }) .then(() => { settled = true; }); await new Promise((resolve) => setImmediate(resolve)); assert.equal(settled, false); assert.equal(ownerSignal.aborted, true); assert.match(ownerSignal.reason.message, /stream end/); releaseCleanup(); await ending; assert.equal(cleanupFinished, true); assert.deepEqual(sent, []); }); test("router shutdown contains rejected asynchronous owner cleanup", async () => { const router = new PluginStreamRouter({ send() {}, onIncomingStream(stream) { stream.bind({ onChunk() {}, async onClose() { throw new Error("cleanup failed"); }, }); return true; }, }); await router.accept({ frame: { streamId: "shutdown-cleanup", sequence: 0, kind: "open", windowBytes: 1_024 } }); router.close(); await new Promise((resolve) => setImmediate(resolve)); }); test("transport failure rejects a write and closes its outgoing stream", async () => { let sends = 0; const router = new PluginStreamRouter({ send() { sends += 1; if (sends > 1) throw new Error("transport closed"); }, }); const stream = await router.openOutgoing("failed-write", 1024); await assert.rejects(stream.write(new Uint8Array([1])), /transport closed/); assert.equal(router.outgoing.size, 0); await assert.rejects(stream.write(new Uint8Array([2])), /closed/); }); test("transport failure while cancelling rejects every backpressured write", async () => { let failSends = false; const router = new PluginStreamRouter({ send() { if (failSends) throw new Error("transport closed"); }, }); const stream = await router.openOutgoing("failed-cancel", 1024); await stream.write(new Uint8Array(1024)); const queued = stream.write(new Uint8Array([1])); failSends = true; assert.throws(() => stream.cancel(), /transport closed/); await assert.rejects(queued, /cancel/); assert.equal(router.outgoing.size, 0); }); test("closing a router invalidates retained incoming release callbacks", async () => { const sent = []; let releaseChunk; let finishChunk; const chunkBlocked = new Promise((resolve) => { finishChunk = resolve; }); const closed = []; let ownerSignal; const router = new PluginStreamRouter({ send(message) { sent.push(message); }, onIncomingStream({ bind, signal }) { ownerSignal = signal; bind({ onChunk(_chunk, release) { releaseChunk = release; return chunkBlocked; }, onClose(reason) { closed.push(reason); }, }); return true; }, }); const contract = await import("@netcatty/plugin-contract"); await router.accept({ frame: { streamId: "closing", sequence: 0, kind: "open", windowBytes: 1024 } }); const accepting = router.accept({ frame: { streamId: "closing", sequence: 1, kind: "chunk", data: contract.createJsonStreamChunk({ value: 1 }), }, }); await new Promise((resolve) => setImmediate(resolve)); const closeError = new Error("runtime closed"); router.close(closeError); assert.equal(ownerSignal.aborted, true); assert.equal(ownerSignal.reason, closeError); releaseChunk(); finishChunk(); await accepting; assert.deepEqual(sent, []); assert.deepEqual(closed, [closeError]); }); test("oversized binary writes fail before consuming stream credit", async () => { const sent = []; const router = new PluginStreamRouter({ send(message) { sent.push(message); } }); const contract = await import("@netcatty/plugin-contract"); const stream = await router.openOutgoing("oversized", contract.PLUGIN_STREAM_MAX_WINDOW_BYTES); await assert.rejects( stream.write(new Uint8Array(contract.PLUGIN_STREAM_MAX_CHUNK_BYTES + 1)), /chunk exceeds/, ); assert.equal(sent.length, 1); assert.equal(router.outgoing.get("oversized").availableBytes, contract.PLUGIN_STREAM_MAX_WINDOW_BYTES); });