Skip to content
TypeWire

@tahanabavi/typesocket

v2.2.0

Contract-driven, type-safe Socket.IO client — one Zod contract validated in both directions, with acks, middleware, queueing and a devtools instrumentation seam.

Middleware

Middleware sees both directions and can observe, rewrite, or drop a frame.

ts
const remove = client.use((frame) => {
console.debug(frame.direction, frame.eventId, frame.payload);

if (frame.direction === "outbound" && isRateLimited(frame.eventId)) {
return false; // drop it
}
if (frame.direction === "outbound") {
return { payload: { ...(frame.payload as object), ts: Date.now() } };
}
});

Returning undefined passes the frame through, false drops it, and { payload } replaces it. Rewrites happen before validation, so a middleware can't smuggle a payload past the contract. A middleware that throws is logged and skipped — it never takes the frame down with it.


Instrumentation & overrides

The seam higher layers build on, mirroring typefetch's client.instrument(). Attaching a hook is the only thing that turns event construction on — with no hook, the path is identical to the un-instrumented one.

ts
const detach = client.instrument({
on(event) {
// "connect" | "disconnect" | "connect_error"
// "outbound" | "ack" | "inbound" | "dropped" | "frame_error"
timeline.push(event);
},
resolveOverride(eventId, payload) {
if (eventId === "chat.sendMessage") {
return { latencyMs: 800, ack: { id: "mocked", sentAt: Date.now() } };
}
},
});

outbound and its ack/frame_error share a frameId, so a panel can pair them. Overrides let a devtools panel change one frame without touching the contract:

FieldEffect
dropDiscard the frame. An emit awaiting an ack then times out, as a lost packet would.
latencyMsDelay the frame.
payloadReplace the payload (value or deriving function).
ackAnswer locally, bypassing the network. Still validated.
errorForce a failure.
request / responseSwap a schema at runtime to test a structural change.

The first hook that returns an override wins for that frame.