@gamiumgamers/packetron
v1.0.18
Published
A simple to use clustered backend framework designed to utilize multiprocessing to improve throughput for nodejs servers while minimizing overhead.
Readme
Packetron
A lean, high-throughput Node.js web framework built directly on the http module. Packetron's design centers on three ideas:
- Multiprocessing first. A primary process forks workers via Node's
clustermodule and lets the OS round-robin connections across them, giving near-linear scaling with CPU count. - Bitwise flags everywhere. Configuration is encoded as integer bit fields rather than option objects, so every per-request control-flow decision is a single
&operation. - Pay-for-what-you-use. Optional features (rate limiting, body size caps, multipart parsing, custom file storage) cost effectively zero when unused — they're guarded by a single truthy check that the branch predictor pins as never-taken.
Table of Contents
- Installation
- Quick Start
- Server:
new Packetron(options) - Routing:
handle() - Static Files:
serve()/serveFile()/unserve() - Request API (
PacketronRequest) - Response API (
PacketronResponse) - Body Parsing Modes
- Multipart / File Uploads
- Rate Limiting & Request Size Limits
- Plugin System
- Global Data (Cross-Worker State)
- Flag Reference
- HTTP Response Codes
- Caveats & Known Limitations
Installation
npm install @gamiumgamers/packetronPacketron depends on busboy, bson, fast-xml-parser, and fastify transitively. Node 18+ recommended (uses ?? and private class fields).
Quick Start
const { Packetron, PacketronServerFlags, PacketronRequestMethod, PacketronHandlerFlags } = require("@gamiumgamers/packetron");
const server = new Packetron({
port: 3000,
maxThreads: 4,
flags: PacketronServerFlags.START_IMMEDIATELY
});
server.handle({
method: PacketronRequestMethod.GET,
routePath: "api/status",
handler: (request, response) => {
response.sendJson({ status: "ok" });
}
});
server.handle({
method: PacketronRequestMethod.POST,
routePath: "api/echo",
flags: PacketronHandlerFlags.JSON_BODY,
handler: async (request, response) => {
const body = await request.getBody();
response.sendJson({ received: body });
}
});Server: new Packetron(options)
| Option | Type | Default | Description |
|---|---|---|---|
| port | number | 3000 | TCP port to bind. |
| host | string | "0.0.0.0" | Bind address. |
| maxThreads | number | os.cpus().length | Worker process count (1 = single-process mode, no fork). |
| flags | number | 0 | Bitwise PacketronServerFlags. |
| plugins | PacketronPlugin[] | [] | Global plugins applied to every request. |
PacketronServerFlags
| Flag | Effect |
|---|---|
| START_IMMEDIATELY | Call .start() from the constructor. |
| STOP_ON_ERROR | Stop the server on any logged error. Use with care — a single bad request can take a worker down. |
| TRY_NEXT_PORT | If the port is in use, increment and retry. |
| SILENCE_INTERNAL_LOGS | Suppress Packetron's own log() output. |
| NO_GLOBAL_DATA | Disable cross-worker global data file. |
| RAISE_PROCESS_PRIORITY | Bump the OS process priority on worker start. |
Lifecycle
await server.start(); // forks workers if maxThreads > 1
server.stop(); // workers ask the primary to broadcast shutdownOn Windows, start() attempts to add a firewall rule for the port (requires UAC the first time). On Linux it tries iptables/ufw with sudo. Both are best-effort and non-fatal.
Routing: handle()
server.handle({
method, // PacketronRequestMethod.*
routePath, // exact path string, no leading slash needed
handler, // (request, response) => any | Promise<any>
plugins, // PacketronPlugin[] (route-local)
flags, // PacketronHandlerFlags bitfield
multipartOptions, // see Multipart section
maxRequestsPerSecond, // optional throttle (per worker)
maxRequestSizeInBytes // optional payload cap (Content-Length + on-stream)
});Returns a PacketronRoute that can be removed later with .remove().
Routes are matched by exact string equality on the normalized path. There is currently no support for path parameters (:id), wildcards, or regex routes.
PacketronRequestMethod
GET POST PUT DELETE HEAD — integer constants used internally as array indices.
PacketronHandlerFlags
| Flag | Body becomes... |
|---|---|
| JSON_BODY | parsed JSON (or null on parse error) |
| FORM_DATA | flat object of fields from application/x-www-form-urlencoded or multipart/form-data (fields only — files are drained and discarded) |
| MULTIPART_FORM_DATA | full multipart parsing — fields in getBody(), files in getFiles() |
| FILE_UPLOAD | legacy file-only mode — getFiles() returns { fieldname: pathString }; getBody() returns "" |
| XML_BODY | parsed XML via fast-xml-parser |
| PLAIN_TEXT_BODY | raw string |
| DONT_USE_BODY | skip body parsing entirely |
| LOCAL_PLUGINS_ONLY | bypass global plugins for this route (reserved — partial wiring) |
Body parsing is chosen at registration time, not dispatched by Content-Type. A route flagged JSON_BODY will not auto-fall-back to text if the client sends something else.
Static Files: serve() / serveFile() / unserve()
server.serve({
directory: path.join(__dirname, "public"),
pathPrefix: "static",
flags: PacketronFileRouterFlags.NON_RECURSIVE,
plugins: [],
maxRequestsPerSecond: 1000,
maxRequestSizeInBytes: null
});
server.serveFile({
routePath: "robots.txt",
filePath: "./robots.txt"
});
server.unserve({ directory: "./public", pathPrefix: "static" });Each file becomes its own endpoint. Per-endpoint rate limits and size caps apply per-file when configured at the serve() / serveFile() level.
PacketronFileRouterFlags
| Flag | Effect |
|---|---|
| NON_RECURSIVE | serve() skips subdirectories. |
| NO_DIRECTORY_STRUCTURE | Flatten — files are reachable by basename under pathPrefix, ignoring nested folders. |
| USER_SPECIFIED_FILE | Set internally by serveFile(). |
| LOCAL_PLUGINS_ONLY | Bypass global plugins for static routes. |
MIME types are inferred from the file extension via a built-in map in PacketronConstants._mimeTypes. Unknown extensions fall back to application/octet-stream.
Request API (PacketronRequest)
Extends Node's IncomingMessage. All public accessors are async.
| Method | Returns | Notes |
|---|---|---|
| getBody() | parsed body per flag, or null if request has no body | Resolves once parsing completes. |
| getFiles() | Object<fieldname, fileRef> | Shape depends on flag (see Multipart section). |
| getQueryParams() | Object<string, string> | Parsed from URL, cached on first call. |
| getCookies() | Object<string, string> | Parsed from Cookie header, cached. |
| getCookie(name) | string | null | Convenience accessor over getCookies(). |
| getIp() | string | Reads X-Forwarded-For then falls back to socket.remoteAddress. |
| hasBody() | boolean | Based on Content-Length / Transfer-Encoding. |
Response API (PacketronResponse)
Extends Node's ServerResponse.
response.sendJson(obj); // sets Content-Type and ends
response.sendHtml("<h1>Hi</h1>");
response.sendTextPlain("hello");
response.send(payload); // auto-deduces JSON for objects
response.sendOk(); // 200, empty
response.sendStatusCode(404); // empty body
response.sendFile(filePath); // streams with MIME from extension
response.downloadFile(filePath); // sets Content-Disposition: attachment
response.setCookie("session", "abc", {
maxAge: 3600,
path: "/",
httpOnly: true,
secure: true,
sameSite: "lax"
});
response.clearCookie("session", { path: "/" });Body Parsing Modes
All body parsing happens via streams during the request lifetime. The chosen parser populates internal state, then getBody() resolves.
JSON_BODY
server.handle({
method: PacketronRequestMethod.POST,
routePath: "api/users",
flags: PacketronHandlerFlags.JSON_BODY,
handler: async (req, res) => {
const body = await req.getBody();
if (body === null) return res.sendStatusCode(400);
res.sendJson(body);
}
});Invalid JSON sets the body to null and logs to stderr — there is no auto-400.
FORM_DATA
Captures fields from application/x-www-form-urlencoded and multipart/form-data. File parts are drained and discarded so the request still completes — use MULTIPART_FORM_DATA if you need the files.
XML_BODY, PLAIN_TEXT_BODY
Accumulate the body in memory and either XML-parse or return the raw string.
Warning:
JSON_BODY,XML_BODY, andPLAIN_TEXT_BODYhave no built-in size cap. Pair them withmaxRequestSizeInBytes(see Rate Limiting & Request Size Limits) on any externally exposed route.
Multipart / File Uploads
MULTIPART_FORM_DATA is the recommended way to handle file uploads in modern code. It captures both fields and files in one pass, streams files to disk with backpressure, and supports configurable limits + a streaming hook for handing files off to remote storage.
server.handle({
method: PacketronRequestMethod.POST,
routePath: "upload",
flags: PacketronHandlerFlags.MULTIPART_FORM_DATA,
multipartOptions: {
maxFileSize: 5 * 1024 * 1024 * 1024, // 5 GB per file
maxFiles: 20,
maxFields: 50,
maxFieldSize: 1024 * 1024, // per-field byte cap
maxFieldNameSize: 100,
maxParts: Infinity,
maxHeaderPairs: 2000,
fileStorageDirectory: "/data/uploads" // optional override
},
handler: async (req, res) => {
const fields = await req.getBody();
const files = await req.getFiles();
res.sendJson({ fields, files });
}
});File entry shape (under MULTIPART_FORM_DATA)
{
path: "/tmp/.packetroninternal/files/avatar_a1b2c3d4_1709999999999.png",
filename: "avatar.png",
encoding: "7bit",
mimeType: "image/png",
size: 38421,
truncated: false
}If multiple files share a fieldname, the value becomes an array. Same rule applies to repeated form fields under getBody().
Streaming hook (skip disk storage)
Provide onFile to take over the stream — useful for piping directly to S3, GCS, or running a hashing transform:
multipartOptions: {
onFile: async (fieldname, fileStream, info) => {
const uploadResult = await uploadToS3(fileStream, info.filename);
return { key: uploadResult.Key, etag: uploadResult.ETag };
// Whatever you return becomes the entry under getFiles()[fieldname].
}
}Limit events
When a configured limit is breached mid-upload, busboy-equivalent events fire on the request:
| Event | Args |
|---|---|
| "multipart-file-limit" | (fieldname, info) |
| "multipart-files-limit" | none |
| "multipart-fields-limit" | none |
| "multipart-parts-limit" | none |
Files that hit maxFileSize are unlinked automatically.
Legacy FILE_UPLOAD flag
Still supported for backwards compatibility. Behavior:
getFiles()returns{ fieldname: "/abs/path/to/file" }(plain string paths).getBody()returns""— fields are not captured.- Filenames keep more of the original (use
MULTIPART_FORM_DATAfor crypto-random sanitized names).
Use MULTIPART_FORM_DATA for new code.
Rate Limiting & Request Size Limits
Both knobs are exposed on handle(), serve(), and serveFile():
server.handle({
method: PacketronRequestMethod.POST,
routePath: "api/upload",
flags: PacketronHandlerFlags.MULTIPART_FORM_DATA,
maxRequestsPerSecond: 100,
maxRequestSizeInBytes: 50 * 1024 * 1024,
handler: async (req, res) => { /* ... */ }
});maxRequestsPerSecond
- Fixed 1-second window per endpoint.
- Per-worker counter — under clustering, the effective cap is
workers × maxRequestsPerSecond. Cross-worker coordination would require IPC on every request and defeat the framework's perf goals. - On breach: responds
429 Too Many RequestswithRetry-After: 1. - Overhead when unused: zero (single null check, branch-predicted away).
- Overhead when used: one
Date.now(), one integer compare, one increment per request (~80–120 ns).
maxRequestSizeInBytes
- Reads
Content-Lengthfirst — if present and over budget, responds413 Payload Too Largebefore reading a single byte of the body. - If
Content-Lengthis absent (chunked transfer-encoding), attaches a singledatalistener that counts bytes and destroys the request on overflow. - Overhead when unused: zero.
- Overhead when used with
Content-Length: ~50 ns. With chunked: one add + compare per chunk.
Both limits can be combined, and either can be set independently. Pass null (the default) to disable.
Plugin System
Plugins are reusable async functions with a numeric priority. A plugin returning true short-circuits — neither subsequent plugins nor the route handler will run.
const { PacketronPlugin, PacketronPluginPriority } = require("@gamiumgamers/packetron");
const authPlugin = PacketronPlugin.create({
priority: PacketronPluginPriority.HIGH,
handler: async (request, response) => {
const token = await request.getCookie("session");
if (!token) {
response.sendStatusCode(401);
return true; // short-circuit
}
}
});Attach globally:
const server = new Packetron({ port: 3000, plugins: [authPlugin] });
// or:
server.insertGlobalPlugin(authPlugin);Or per-route:
server.handle({
routePath: "api/secret",
plugins: [authPlugin],
handler: (req, res) => res.sendJson({ ok: true })
});PacketronPluginPriority
EXTEMELY_LOW (0) ... MEDIUM (4) ... EXTEMELY_HIGH (8). Higher priorities run first within their scope. Note the spelling — EXTEMELY is preserved for API stability.
A single plugin instance can be registered with multiple managers; plugin.setPriority(p) re-sorts every manager it's attached to. plugin.remove(manager) detaches from a specific manager, or from all if called with no arg.
Global Data (Cross-Worker State)
server.setGlobalData("counter", 42);
const value = server.getGlobalData("counter");Backed by a BSON file under the OS temp directory, shared across all workers and any other Packetron servers on the same machine.
Caveat: the read/write is synchronous and unlocked. Concurrent writers can lose updates. Treat it as a low-frequency configuration store, not a counter or queue. Use a real KV store (Redis, etc.) for hot state.
Pass PacketronServerFlags.NO_GLOBAL_DATA to disable the file entirely.
Flag Reference
PacketronServerFlags
START_IMMEDIATELY = 1 << 0
STOP_ON_ERROR = 1 << 1
TRY_NEXT_PORT = 1 << 2
SILENCE_INTERNAL_LOGS = 1 << 3
NO_GLOBAL_DATA = 1 << 4
RAISE_PROCESS_PRIORITY = 1 << 5PacketronHandlerFlags
JSON_BODY = 1 << 0
DONT_USE_BODY = 1 << 1
FILE_UPLOAD = 1 << 2
LOCAL_PLUGINS_ONLY = 1 << 3
FORM_DATA = 1 << 4
XML_BODY = 1 << 5
PLAIN_TEXT_BODY = 1 << 6
MULTIPART_FORM_DATA = 1 << 7PacketronFileRouterFlags
NON_RECURSIVE = 1 << 0
NO_DIRECTORY_STRUCTURE = 1 << 1
USER_SPECIFIED_FILE = 1 << 2
LOCAL_PLUGINS_ONLY = 1 << 3Combine with |:
flags: PacketronHandlerFlags.JSON_BODY | PacketronHandlerFlags.LOCAL_PLUGINS_ONLYHTTP Response Codes
HttpResponseCode exports named constants for the standard 1xx–5xx codes. Use them with response.sendStatusCode(...):
const { HttpResponseCode } = require("@gamiumgamers/packetron");
response.sendStatusCode(HttpResponseCode.UNAUTHORIZED); // 401Common ones: OK (200), CREATED (201), NO_CONTENT (204), BAD_REQUEST (400), UNAUTHORIZED (401), FORBIDDEN (403), NOT_FOUND (404), PAYLOAD_TOO_LARGE (413), TOO_MANY_REQUESTS (429), INTERNAL_SERVER_ERROR (500).
Caveats
Honest disclosure — things to know before deploying:
- No path parameters or regex routes. Routing is exact-match string equality. A route like
/users/:idmust be implemented as a global plugin that inspectsrequest.url. - No
Content-Typedispatch. The body parser is chosen at registration via a flag; the client cannot influence it. - No built-in CORS, CSRF, or security headers. Implement via plugins.
JSON_BODY/XML_BODY/PLAIN_TEXT_BODYbuffer the entire body in memory. Always pair them withmaxRequestSizeInByteson externally exposed routes.- Rate limits are per-worker. With 4 workers and
maxRequestsPerSecond: 100, the cluster will accept up to 400 req/s for that endpoint. - Global data API is sync + unlocked. Don't use it as a hot-path data structure.
STOP_ON_ERRORis dangerous. A single logged error will kill the worker; the primary will respawn but in-flight requests are lost. Leave it off for production.
License
ISC — see package.json.
