UNPKG

rxprotoplex

Version:

A utility library for working with Plex-based connections and streams with RxJS operators.

446 lines (377 loc) 13.5 kB
import {test, solo, skip} from "brittle"; import { listenAndConnectionAndRead$, createPlexPair, connectAndRead$, withEncoding, withPlex, withHandshake, listenAndConnection$, connectAndSend, tapSend, connection$, close$, connectionAndRead$, connect$, asPlex, destroy, encodingFrom } from "./index.js"; import b4a from "b4a"; import {from, take} from "rxjs"; import {withTimeout} from "./lib/withTimeout.js"; import {Duplex} from "streamx"; import {catchError} from "rxjs/operators"; import cenc from "compact-encoding"; // Utility function for delayed closure const delayedClose = (closure$, delay, value) => { setTimeout(() => { closure$.next(value); }, delay); }; test("destroy function with error", async (t) => { t.plan(2); // Create a Plex instance using Duplex (or your specific implementation) const plex = asPlex(new Duplex()); // Subscribe to the close$ observable const subscription = close$(plex).subscribe({ next(e) { t.is(e.message, "fun", 'Next message for close$ should be an error for "fun"'); }, error(e) { t.is(e.message, "fun", 'Error message should match "fun"'); }, complete() { t.fail('Should not complete when destroy is called with error'); }, }); // Call destroy with an error to trigger the 'error' event destroy(plex, new Error("fun")); // Cleanup after test to prevent memory leaks subscription.unsubscribe(); }); test("destroy function without error", async (t) => { t.plan(2); // Create a Plex instance using Duplex (or your specific implementation) const plex = asPlex(new Duplex()); // Subscribe to the close$ observable const subscription = close$(plex).subscribe({ next(value) { t.absent(value, 'Should receive next when destroy is called without error and the value is undefined'); }, error(e) { t.fail('Error should not occur'); }, complete() { t.pass('Should not complete when destroy is called with error'); }, }); // Call destroy with an error to trigger the 'error' event destroy(plex); // Cleanup after test to prevent memory leaks subscription.unsubscribe(); }); skip("close$ emits when stream destroys", t => { t.plan(1) const plex = asPlex(new Duplex()); plex.close$.subscribe(() => { t.pass(); }); destroy(plex); }); test("test connection$", async t => { t.plan(1); const [p1, p2] = createPlexPair(); p1.listen(); connectionAndRead$(p1).pipe(take(1)).subscribe( ({data}) => t.is(b4a.toString(data), "hello") ); p2.connect().end(b4a.from("hello")); }); test("has isProtoplex", t => { const [p1,p2] = createPlexPair() t.is(p1.isProtoplex && p2.isProtoplex, true); const socket = p1.connect(); t.absent(socket.isProtoplex); t.absent(p1.mux.stream.isProtoplex); socket.destroy(); }) test("test listenAndConnect$", async t => { t.plan(1); const [p1, p2] = createPlexPair(); listenAndConnectionAndRead$(p1).pipe(take(1)).subscribe( ({data}) => t.is(b4a.toString(data), "hello") ); p2.connect().end(b4a.from("hello")); }); test("test connectAndSend and withPlex with encoding", async t => { t.plan(1); const [p1, p2] = createPlexPair(); let sender; withPlex(p2, () => { sender = connectAndSend({encoding: "utf8"}); withPlex(p1, () => { listenAndConnectionAndRead$({encoding: "utf8"}).pipe(take(1)).subscribe( ({data}) => { t.is(data, "hello"); } ) }); }); sender("hello") }); test("withHandshake and withEncoding succeeds", async t => { t.plan(1); const [p1, p2] = createPlexPair(); const config = withEncoding("utf8") const serverConfig = withHandshake({ handshakeEncoding: "utf8", handshake: "bob", onhandshake: (o) => o === "alice" }, config); const clientConfig = withHandshake({ handshakeEncoding: "utf8", handshake: "alice", onhandshake: (o) => o === "bob" }, config); listenAndConnectionAndRead$(p1, serverConfig).pipe(take(1)).subscribe( ({data}) => { t.is(data, "hello"); } ); const sender2 = connectAndSend(p2, clientConfig); sender2("hello"); }); test("withHandshake and withEncoding rejection", async t => { t.plan(1); const [p1, p2] = createPlexPair(); const config = withEncoding("utf8") const serverConfig = withHandshake({ handshakeEncoding: "utf8", handshake: "bob", onhandshake: (o) => o === "alice" }, config); const clientConfig = withHandshake({ handshakeEncoding: "utf8", handshake: "wonderland", onhandshake: (o) => o === "bob" }, config); listenAndConnectionAndRead$(p1, serverConfig).pipe(take(1)).subscribe( { next: () => { t.fail("Rejected in handshake so this shouldn't happen."); }, error: (e) => { t.is(e.message, "Connection Rejected!", "Connection should be rejected."); } } ); connectAndSend(p2, clientConfig)("won't happen"); }); test("tapSend and withEncoding", async t => { t.plan(2); const serverTest = t.test("server"); serverTest.plan(3); const [p1, p2] = createPlexPair(); const original = ["hello", "world", "!"]; const serverResult = []; const clientResult = []; listenAndConnectionAndRead$(p1, withEncoding("utf8")) .subscribe(({data}) => { const orig = original.shift(); serverResult.push(orig) serverTest.is(data, orig); } ); from(original).pipe(tapSend(p2, withEncoding("utf8"))) .subscribe((data) => { clientResult.push(data); }); await serverTest; t.alike(serverResult, clientResult); }); test("connect and read", async t => { t.plan(3); const [p1, p2] = createPlexPair(); withPlex(p1, () => { listenAndConnection$(withEncoding("json")).subscribe( stream => { stream.write({hello: "world"}); stream.write("no worries"); stream.write({hello: "world"}); } ); }); withPlex(p2, () => { connectAndRead$(withEncoding("json")).subscribe( ({data: o}) => { if (typeof o === "object") t.alike(o, {hello: "world"}); else t.is(o, "no worries"); }); }); }); test("should establish multiple connections and exchange messages", async t => { t.plan(2); // Expect two successful assertions const [p1, p2] = createPlexPair(); const conn$ = listenAndConnection$(p1, "channelId"); conn$.subscribe({ next: connection => { connection.on("data", data => { t.is(b4a.toString(data), "test message", "Message delivered successfully"); }); }, error: () => t.fail("Error should not occur during connection"), complete: () => t.fail("Complete should not be called in this test") }); // Establish a connection from p2 and send a message connect$(p2, "channelId").subscribe(stream => { stream.end(b4a.from("test message")); t.pass("Connection successfully established from p2"); }); }); test("should establish connection and exchange empty messages", async t => { t.plan(2); const [p1, p2] = createPlexPair(); const conn$ = listenAndConnection$(p1, "channelId"); conn$.subscribe({ next: connection => { connection.on("data", data => { t.ok(b4a.equals(data, b4a.alloc(0)), "Message delivered successfully"); }); }, error: () => t.fail("Error should not occur during connection"), complete: () => t.fail("Complete should not be called in this test") }); // Establish a connection from p2 and send a message connect$(p2, "channelId").subscribe(stream => { stream.end(b4a.alloc(0)); t.pass("Connection successfully established from p2"); }); }); test("should handle connection error for non-existent channel", async t => { t.plan(1); // Expect one assertion const [p1, p2] = createPlexPair(); // Wrap the connect$ observable with a timeout const timeoutDuration = 1000; // Set timeout to 1000 ms for test withTimeout(connect$(p1, "nonExistentChannel"), timeoutDuration).subscribe({ next: () => t.fail("Next should not be called for non-existent channel"), error: (err) => { t.ok(err, "Error caught successfully when connecting to a non-existent channel"); }, complete: () => t.fail("Complete should not be called for non-existent channel") }); }); test("connection$ closes properly on close$ with error", async t => { t.plan(3); const [p1, p2] = createPlexPair(); const conn$ = listenAndConnection$(p1, "channelId"); const closure$ = close$(p1); p1.close$.subscribe({error: e => t.is(e.message, "Error!!!!")}); p2.close$.subscribe({error: e => t.is(e.message, "Error!!!!")}); conn$.subscribe({ next: connection => { t.ok(connection, "Connection established successfully"); } }); connect$(p2, "channelId").subscribe({ next: stream => { stream.end(b4a.from("test message")); } }); setTimeout(() => { // Non graceful close.... error closure$.next(new Error("Error!!!!")); }); }); test("connection$ closes properly on close$ gracefully", async t => { t.plan(3); const [p1, p2] = createPlexPair(); const conn$ = listenAndConnection$(p1, "channelId"); const closure$ = close$(p1); p1.close$.subscribe(t.absent); p2.close$.subscribe(t.absent); conn$.subscribe({ next: connection => { t.ok(connection, "Connection established successfully"); } }); connect$(p2, "channelId").subscribe({ next: stream => { stream.end(b4a.from("test message")); } }); setTimeout(() => { // graceful close without error. closure$.next(); }); }); test("should gracefully shutdown on close$ while messages are exchanged", async t => { t.plan(2); const [p1, p2] = createPlexPair(); const conn$ = listenAndConnection$(p1, "channelId"); const closure$ = close$(p1); conn$.subscribe({ next: connection => { connection.on("data", data => { t.is(b4a.toString(data), "test message", "Message delivered successfully"); }); }, complete: () => { t.pass("Connection closed gracefully"); }, error: (err) => { t.fail("Unexpected error during graceful shutdown: " + err.message); } }); // Establish connection and send messages connect$(p2, "channelId").subscribe({ next: stream => { stream.write(b4a.from("test message")); } }); // Emit close$ without an error after a delay to simulate graceful shutdown setTimeout(() => { closure$.next(); // Trigger closure without an error }, 100); }); test("should establish multiple connections and exchange messages", async t => { t.plan(2); // Expect two successful assertions const [p1, p2] = createPlexPair(); const largeData = b4a.from("x".repeat(1024 * 1024)); const conn$ = listenAndConnection$(p1, "channelId"); conn$.subscribe({ next: async connection => { for await (const data of connection) t.alike(data, largeData); }, error: () => t.fail("Error should not occur during connection"), complete: () => t.fail("Complete should not be called in this test") }); connectAndSend(p2, "channelId")(largeData); // // Establish a connection from p2 and send a message connect$(p2, "channelId").subscribe(stream => { stream.write(b4a.from("x".repeat(1024 * 1024))); // t.pass("Connection successfully established from p2"); }); }); // TODO: trace where the data is left open. // solo("should gracefully clean up resources and event listeners after close$", async t => { // t.plan(1); // const [p1] = createPlexPair(); // // const conn$ = listenAndConnection$(p1, "channelId"); // const closure$ = close$(p1); // // conn$.subscribe({ // complete: () => { // // Check for any lingering event listeners after connection closure // setTimeout(() => { // const listeners = p1.mux.stream.listenerCount("data"); // if (listeners === 0) { // t.pass("All resources cleaned up successfully"); // } else { // t.fail(`Unexpected lingering listeners detected: ${listeners}`); // } // },1000); // }, // error: (err) => t.fail("Unexpected error during resource cleanup: " + err.message) // }); // // // Trigger graceful closure after a delay to simulate graceful shutdown // setTimeout(() => { // closure$.next(); // Trigger closure without an error (graceful shutdown) // }, 2000); // });