diff --git a/.machine_readable/AGENTIC.scm b/.machine_readable/AGENTIC.scm index 651bc74..ad595dd 100644 --- a/.machine_readable/AGENTIC.scm +++ b/.machine_readable/AGENTIC.scm @@ -1,5 +1,5 @@ ;; SPDX-License-Identifier: AGPL-3.0-or-later -;; AGENTIC.scm - AI agent interaction patterns for rsr-template-repo +;; AGENTIC.scm - AI agent interaction patterns for rescript-websocket (define agentic-config `((version . "1.0.0") @@ -12,5 +12,6 @@ (refactoring . "conservative") (testing . "comprehensive"))) (constraints - ((languages . ()) - (banned . ("typescript" "go" "python" "makefile")))))) + ((languages . ("rescript" "bash")) + (banned . ("typescript" "go" "python" "node" "npm" "bun")) + (runtime . "deno-only"))))) diff --git a/.machine_readable/ECOSYSTEM.scm b/.machine_readable/ECOSYSTEM.scm index 23e27df..056d0d8 100644 --- a/.machine_readable/ECOSYSTEM.scm +++ b/.machine_readable/ECOSYSTEM.scm @@ -1,20 +1,36 @@ ;; SPDX-License-Identifier: AGPL-3.0-or-later -;; ECOSYSTEM.scm - Ecosystem position for rsr-template-repo +;; ECOSYSTEM.scm - Ecosystem position for rescript-websocket ;; Media-Type: application/vnd.ecosystem+scm (ecosystem (version "1.0") - (name "rsr-template-repo") - (type "") - (purpose "") + (name "rescript-websocket") + (type "library") + (purpose "Type-safe WebSocket bindings for ReScript applications") (position-in-ecosystem - (category "") - (subcategory "") - (unique-value ())) + (category "networking") + (subcategory "websocket") + (unique-value + ("First-class ReScript support" + "Deno-native implementation" + "Zero runtime overhead" + "Type-safe ready state handling"))) - (related-projects ()) + (related-projects + (("rescript-fetch" . "HTTP client bindings") + ("rescript-deno" . "Deno runtime bindings") + ("rescript-json-schema" . "JSON Schema validation") + ("rescript-full-stack" . "Parent ecosystem"))) - (what-this-is ()) + (what-this-is + ("WebSocket client bindings" + "WebSocket server bindings for Deno" + "Broadcast utilities" + "Type-safe event handling")) - (what-this-is-not ())) + (what-this-is-not + ("Full Socket.IO replacement" + "Message persistence layer" + "Authentication system" + "Horizontal scaling solution"))) diff --git a/.machine_readable/META.scm b/.machine_readable/META.scm index 9df6e6d..38118b5 100644 --- a/.machine_readable/META.scm +++ b/.machine_readable/META.scm @@ -1,17 +1,30 @@ ;; SPDX-License-Identifier: AGPL-3.0-or-later -;; META.scm - Meta-level information for rsr-template-repo +;; META.scm - Meta-level information for rescript-websocket ;; Media-Type: application/meta+scheme (meta - (architecture-decisions ()) + (architecture-decisions + (("Zero-cost FFI" . "Direct bindings to WebSocket API with no wrapper overhead") + ("Type-safe ready states" . "Enum for connection states instead of magic numbers") + ("Deno-first" . "Designed for Deno runtime, not Node.js") + ("Pipe-first API" . "Functions designed for -> operator"))) (development-practices - (code-style ()) + (code-style + ("@@uncurried for all modules" + "Pipe-first syntax preferred" + "Doc comments on public API")) (security - (principle "Defense in depth")) - (testing ()) + (principle "Secure by default") + (notes "WSS preferred, no credential storage")) + (testing + ("Deno test runner" + "Integration tests with real WebSocket")) (versioning "SemVer") (documentation "AsciiDoc") (branching "main for stable")) - (design-rationale ())) + (design-rationale + (("External bindings" . "Use @external for direct JS interop") + ("Callback-based events" . "Match WebSocket API semantics") + ("Optional server" . "Client works standalone without server module")))) diff --git a/.machine_readable/PLAYBOOK.scm b/.machine_readable/PLAYBOOK.scm index 23d2745..bca2480 100644 --- a/.machine_readable/PLAYBOOK.scm +++ b/.machine_readable/PLAYBOOK.scm @@ -1,13 +1,20 @@ ;; SPDX-License-Identifier: AGPL-3.0-or-later -;; PLAYBOOK.scm - Operational runbook for rsr-template-repo +;; PLAYBOOK.scm - Operational runbook for rescript-websocket (define playbook `((version . "1.0.0") (procedures - ((deploy . (("build" . "just build") - ("test" . "just test") - ("release" . "just release"))) - (rollback . ()) - (debug . ()))) - (alerts . ()) - (contacts . ()))) + ((build . (("compile" . "just build") + ("watch" . "just dev"))) + (test . (("unit" . "just test") + ("coverage" . "just test-coverage"))) + (release . (("build" . "just build-release") + ("publish" . "deno publish"))) + (debug . (("repl" . "deno repl") + ("inspect" . "deno run --inspect"))))) + (alerts + ((build-failure . "Check ReScript compiler output") + (test-failure . "Review test logs"))) + (contacts + ((maintainer . "hyperpolymath@proton.me") + (issues . "github.com/hyperpolymath/rescript-websocket/issues"))))) diff --git a/.machine_readable/STATE.scm b/.machine_readable/STATE.scm index 1f57360..3569edf 100644 --- a/.machine_readable/STATE.scm +++ b/.machine_readable/STATE.scm @@ -1,39 +1,63 @@ ;; SPDX-License-Identifier: AGPL-3.0-or-later -;; STATE.scm - Project state for rsr-template-repo +;; STATE.scm - Project state for rescript-websocket ;; Media-Type: application/vnd.state+scm (state (metadata - (version "0.0.1") + (version "0.1.0") (schema-version "1.0") - (created "2026-01-03") - (updated "2026-01-03") - (project "rsr-template-repo") - (repo "github.com/hyperpolymath/rsr-template-repo")) + (created "2025-01-01") + (updated "2025-01-04") + (project "rescript-websocket") + (repo "github.com/hyperpolymath/rescript-websocket")) (project-context - (name "rsr-template-repo") - (tagline "") - (tech-stack ())) + (name "rescript-websocket") + (tagline "Type-safe WebSocket client and server for ReScript") + (tech-stack ("ReScript" "Deno" "WebSocket"))) (current-position - (phase "initial") - (overall-completion 0) - (components ()) - (working-features ())) + (phase "initial-release") + (overall-completion 60) + (components + (("client" . 100) + ("server" . 100) + ("broadcast" . 100) + ("tests" . 0) + ("docs" . 80))) + (working-features + ("WebSocket client connection" + "Event handlers (open, close, message, error)" + "Text and binary message sending" + "JSON message helper" + "Ready state checking" + "Promise-based waitForOpen" + "Server-side WebSocket upgrade" + "Deno.serve integration" + "Multi-client broadcast"))) (route-to-mvp - (milestones ())) + (milestones + (("v0.1.0" . "Core client and server bindings") + ("v0.2.0" . "Room/channel abstraction") + ("v0.3.0" . "Protocol support") + ("v1.0.0" . "Stable release")))) (blockers-and-issues (critical) (high) - (medium) - (low)) + (medium + ("Test suite not yet implemented")) + (low + ("JSR publication pending"))) (critical-next-actions - (immediate) - (this-week) - (this-month)) + (immediate + ("Write basic test suite")) + (this-week + ("Publish to JSR")) + (this-month + ("Add room/channel abstraction"))) - (session-history ())) + (session-history + (("2025-01-04" . "Initial implementation complete")))) diff --git a/CONTRIBUTING.adoc b/CONTRIBUTING.adoc index 5da21b5..f0948e1 100644 --- a/CONTRIBUTING.adoc +++ b/CONTRIBUTING.adoc @@ -1,20 +1,208 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -= Contributing Guide +// SPDX-FileCopyrightText: 2025 Hyperpolymath + += Contributing to rescript-websocket +:toc: + +Thank you for your interest in contributing to rescript-websocket! == Getting Started +=== Prerequisites + +* Deno 2.x +* ReScript 11.x (installed via Deno) +* Git with GPG signing configured + +=== Setup + +[source,bash] +---- +# Clone the repository +git clone https://github.com/hyperpolymath/rescript-websocket.git +cd rescript-websocket + +# Install dependencies +deno install + +# Build +just build + +# Run tests +just test +---- + +== Development Workflow + +=== Branch Naming + +* `feature/` - New features +* `fix/` - Bug fixes +* `docs/` - Documentation only +* `refactor/` - Code refactoring +* `test/` - Test additions/improvements + +=== Making Changes + 1. Fork the repository 2. Create a feature branch from `main` -3. Sign off commits (`git commit -s`) -4. Submit a pull request +3. Make your changes +4. Run `just quality` to verify +5. Sign off commits (`git commit -s`) +6. Submit a pull request + +== Code Guidelines + +=== Language Policy + +**ReScript only** - This project follows the https://github.com/hyperpolymath[Hyperpolymath] language policy: + +* ✅ ReScript for all application code +* ✅ Deno for runtime and package management +* ❌ No TypeScript +* ❌ No Node.js/npm/bun + +=== Code Style + +* Use `@@uncurried` for all ReScript files +* Prefer pipe-first syntax (`->`) +* Keep functions pure where possible +* Use descriptive names, avoid abbreviations +* Add doc comments for public API functions + +=== Example + +[source,rescript] +---- +@@uncurried + +/** Send a message if the connection is open */ +let safeSend = (ws: Client.t, message: string): result => { + if Client.isOpen(ws) { + ws->Client.send(message) + Ok() + } else { + Error("Connection not open") + } +} +---- == Commit Guidelines -* Conventional commits: `type(scope): description` -* Sign all commits (DCO required) -* Atomic, focused commits +=== Format + +We use https://www.conventionalcommits.org/[Conventional Commits]: + +---- +type(scope): description + +[optional body] + +[optional footer] +Signed-off-by: Your Name +---- + +=== Types + +* `feat` - New feature +* `fix` - Bug fix +* `docs` - Documentation +* `style` - Formatting, no code change +* `refactor` - Code restructuring +* `test` - Adding tests +* `chore` - Maintenance tasks + +=== Examples + +---- +feat(client): add sendJson helper for JSON serialization + +fix(server): handle connection upgrade failure gracefully + +docs(readme): add reconnection pattern example +---- + +=== DCO Sign-off + +All commits must be signed off (Developer Certificate of Origin): + +[source,bash] +---- +git commit -s -m "feat(client): add new feature" +---- + +== Testing + +=== Running Tests + +[source,bash] +---- +# Run all tests +just test + +# Run specific test file +deno test tests/client_test.res.js + +# Watch mode +just test-watch +---- + +=== Writing Tests + +Tests should be in the `tests/` directory with `_test.res` suffix: + +[source,rescript] +---- +// tests/client_test.res +open WebSocket + +let testClientCreation = () => { + let ws = Client.make("wss://example.com") + assert(Client.isConnecting(ws)) +} +---- + +== Pull Request Process + +1. Ensure all tests pass: `just quality` +2. Update documentation if needed +3. Add entry to CHANGELOG if significant +4. Request review from maintainers +5. Address review feedback +6. Squash commits if requested + +== Issue Reporting + +=== Bug Reports + +Please include: + +* ReScript version +* Deno version +* Minimal reproduction code +* Expected vs actual behavior +* Error messages/stack traces + +=== Feature Requests + +Please include: + +* Use case description +* Proposed API (if any) +* Alternative solutions considered + +== Licence + +By contributing, you agree that your contributions will be licensed under AGPL-3.0-or-later. + +== Questions? + +* Open a GitHub Issue for questions +* Check existing issues before creating new ones +* Be respectful and constructive -== License +== Acknowledgements -Contributions licensed under project license. +Contributors will be acknowledged in the project README. +Thank you for helping make rescript-websocket better! diff --git a/README.adoc b/README.adoc index 3bbfa2b..5c8ec09 100644 --- a/README.adoc +++ b/README.adoc @@ -2,11 +2,470 @@ // SPDX-FileCopyrightText: 2025 Hyperpolymath = rescript-websocket +:toc: left +:toclevels: 3 +:icons: font +:source-highlighter: rouge -**Type-safe WebSocket client and server for ReScript using Deno's native WebSocket API.** +image:https://img.shields.io/badge/ReScript-v11-blue[ReScript v11] +image:https://img.shields.io/badge/Deno-2.x-black[Deno 2.x] +image:https://img.shields.io/badge/license-AGPL--3.0--or--later-blue[License: AGPL-3.0-or-later] + +**Type-safe WebSocket client and server bindings for ReScript using Deno's native WebSocket API.** Part of the https://github.com/hyperpolymath/rescript-full-stack[ReScript Full Stack] ecosystem. +== Overview + +`rescript-websocket` provides zero-cost FFI bindings to the standard WebSocket API, designed to work seamlessly with Deno's runtime. It offers: + +* **Type-safe Client API** - Connect to WebSocket servers with full type safety +* **Server-side Support** - Upgrade HTTP connections to WebSocket using Deno.serve +* **Broadcast Utilities** - Helper functions for multi-client messaging +* **Promise-based Flow** - `waitForOpen` for clean async connection handling +* **Zero Runtime Overhead** - Compiles to direct JavaScript WebSocket calls + +== Installation + +=== Using Deno (Recommended) + +Add to your `deno.json` imports: + +[source,json] +---- +{ + "imports": { + "@hyperpolymath/rescript-websocket": "jsr:@hyperpolymath/rescript-websocket@^0.1.0" + } +} +---- + +=== Manual Installation + +Clone the repository and add to your ReScript project: + +[source,bash] +---- +git clone https://github.com/hyperpolymath/rescript-websocket.git +---- + +Add to `rescript.json`: + +[source,json] +---- +{ + "bs-dependencies": [ + "@hyperpolymath/rescript-websocket" + ] +} +---- + +== Quick Start + +=== Client Example + +[source,rescript] +---- +open WebSocket + +// Create a connection +let ws = Client.make("wss://echo.websocket.org") + +// Set up event handlers +ws->Client.onOpen(() => { + Console.log("Connected!") + ws->Client.send("Hello, WebSocket!") +}) + +ws->Client.onMessage(event => { + Console.log("Received: " ++ event.data) +}) + +ws->Client.onClose(event => { + Console.log(`Closed: ${event.code->Int.toString} - ${event.reason}`) +}) + +ws->Client.onError(error => { + Console.error("Error: " ++ error.message) +}) +---- + +=== Server Example (Deno) + +[source,rescript] +---- +open WebSocket + +// Track connected clients +let clients: ref> = ref([]) + +// Create WebSocket handler +let wsHandler = Server.makeHandler( + ~onConnect=socket => { + Console.log("Client connected") + clients := clients.contents->Array.concat([socket]) + }, + ~onMessage=(socket, data) => { + Console.log("Received: " ++ data) + // Echo back to sender + socket->Client.send("Echo: " ++ data) + // Or broadcast to all clients + broadcast(clients.contents, data) + }, + ~onClose=(_socket, event) => { + Console.log(`Client disconnected: ${event.code->Int.toString}`) + }, +) + +// Start server +let _ = Server.serve( + { + port: 8080, + onListen: info => Console.log(`Server running on ${info["hostname"]}:${info["port"]->Int.toString}`), + }, + async request => { + switch wsHandler(request) { + | Some(response) => response + | None => Fetch.Response.make("Not a WebSocket request") + } + }, +) +---- + +== API Reference + +=== Client Module + +==== Types + +[source,rescript] +---- +type readyState = + | Connecting // 0 - Connection not yet open + | Open // 1 - Connection is open + | Closing // 2 - Connection is closing + | Closed // 3 - Connection is closed + +type t // WebSocket instance + +type closeEvent = { + code: int, + reason: string, + wasClean: bool, +} + +type messageEvent = { data: string } +type errorEvent = { message: string } +---- + +==== Functions + +[cols="1,2,1"] +|=== +|Function |Description |Signature + +|`make` +|Create a new WebSocket connection +|`string => t` + +|`makeWithProtocols` +|Create with subprotocol negotiation +|`(string, array) => t` + +|`readyState` +|Get current connection state +|`t => readyState` + +|`url` +|Get the WebSocket URL +|`t => string` + +|`protocol` +|Get negotiated subprotocol +|`t => string` + +|`bufferedAmount` +|Get bytes queued for sending +|`t => int` + +|`send` +|Send a text message +|`(t, string) => unit` + +|`sendArrayBuffer` +|Send binary data +|`(t, ArrayBuffer.t) => unit` + +|`sendJson` +|Send JSON data (serialized) +|`(t, JSON.t) => unit` + +|`close` +|Close the connection +|`t => unit` + +|`closeWithCode` +|Close with code and reason +|`(t, int, string) => unit` + +|`onOpen` +|Set connection opened handler +|`(t, unit => unit) => unit` + +|`onClose` +|Set connection closed handler +|`(t, closeEvent => unit) => unit` + +|`onMessage` +|Set message received handler +|`(t, messageEvent => unit) => unit` + +|`onError` +|Set error handler +|`(t, errorEvent => unit) => unit` + +|`isOpen` +|Check if connection is open +|`t => bool` + +|`isConnecting` +|Check if still connecting +|`t => bool` + +|`isClosed` +|Check if connection is closed +|`t => bool` + +|`waitForOpen` +|Promise that resolves when open +|`t => promise` +|=== + +=== Server Module + +==== Types + +[source,rescript] +---- +type socket = Client.t // Server-side WebSocket is same as client + +type server // Deno HTTP server handle + +type request // Incoming HTTP request + +type serveOptions = { + port?: int, + hostname?: string, + onListen?: {"port": int, "hostname": string} => unit, +} + +type handler = request => promise +---- + +==== Functions + +[cols="1,2"] +|=== +|Function |Description + +|`upgradeWebSocket` +|Upgrade HTTP request to WebSocket (returns socket + response) + +|`serve` +|Start HTTP/WebSocket server with options + +|`serveHandler` +|Start server with just a handler function + +|`shutdown` +|Gracefully shutdown the server + +|`requestUrl` +|Get URL from request object + +|`makeHandler` +|Create a WebSocket handler with callbacks +|=== + +=== Broadcast Utilities + +[source,rescript] +---- +// Send text message to multiple clients +let broadcast: (array, string) => unit + +// Send JSON to multiple clients +let broadcastJson: (array, JSON.t) => unit +---- + +== Close Codes + +Standard WebSocket close codes: + +[cols="1,2"] +|=== +|Code |Meaning + +|1000 +|Normal closure + +|1001 +|Going away (e.g., page navigation) + +|1002 +|Protocol error + +|1003 +|Unsupported data type + +|1006 +|Abnormal closure (no close frame) + +|1007 +|Invalid frame payload data + +|1008 +|Policy violation + +|1009 +|Message too big + +|1010 +|Mandatory extension missing + +|1011 +|Internal server error + +|1015 +|TLS handshake failure +|=== + +== Patterns & Best Practices + +=== Reconnection with Exponential Backoff + +[source,rescript] +---- +let rec connect = (url, attempt) => { + let ws = Client.make(url) + + ws->Client.onClose(_ => { + let delay = Math.min(1000.0 *. Math.pow(2.0, attempt->Int.toFloat), 30000.0) + Console.log(`Reconnecting in ${delay->Float.toString}ms...`) + let _ = setTimeout(() => connect(url, attempt + 1), delay->Float.toInt) + }) + + ws->Client.onOpen(() => { + Console.log("Connected!") + // Reset attempt counter on successful connection + }) + + ws +} + +let ws = connect("wss://example.com/ws", 0) +---- + +=== Type-Safe JSON Messages + +[source,rescript] +---- +type message = + | Ping + | Pong + | Chat({user: string, text: string}) + | Join({room: string}) + +let encodeMessage = msg => { + switch msg { + | Ping => JSON.Encode.object([("type", JSON.Encode.string("ping"))]) + | Pong => JSON.Encode.object([("type", JSON.Encode.string("pong"))]) + | Chat({user, text}) => + JSON.Encode.object([ + ("type", JSON.Encode.string("chat")), + ("user", JSON.Encode.string(user)), + ("text", JSON.Encode.string(text)), + ]) + | Join({room}) => + JSON.Encode.object([ + ("type", JSON.Encode.string("join")), + ("room", JSON.Encode.string(room)), + ]) + } +} + +ws->Client.sendJson(encodeMessage(Chat({user: "alice", text: "Hello!"}))) +---- + +== Ecosystem Integration + +This library is part of the https://github.com/hyperpolymath/rescript-full-stack[ReScript Full Stack] ecosystem: + +* **rescript-websocket** - WebSocket client/server (this library) +* **rescript-fetch** - HTTP client bindings +* **rescript-deno** - Deno runtime bindings +* **rescript-json-schema** - JSON Schema validation + +== Development + +=== Prerequisites + +* Deno 2.x +* ReScript 11.x + +=== Build + +[source,bash] +---- +# Install dependencies +deno install + +# Build ReScript +just build +# or +deno task build + +# Watch mode +just dev +# or +deno task dev +---- + +=== Test + +[source,bash] +---- +just test +---- + +=== Format & Lint + +[source,bash] +---- +just fmt +just lint +---- + +== Contributing + +See link:CONTRIBUTING.adoc[CONTRIBUTING.adoc] for guidelines. + +* Sign all commits (DCO required) +* Follow conventional commits +* ReScript only - no TypeScript + == Licence AGPL-3.0-or-later + +Copyright (C) 2025 Hyperpolymath + +This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + +See link:LICENSE.txt[LICENSE.txt] for full text. + +== See Also + +* https://developer.mozilla.org/en-US/docs/Web/API/WebSocket[MDN WebSocket API] +* https://docs.deno.com/runtime/fundamentals/websockets/[Deno WebSocket Guide] +* https://rescript-lang.org/[ReScript Documentation] diff --git a/ROADMAP.adoc b/ROADMAP.adoc index feb9954..961d085 100644 --- a/ROADMAP.adoc +++ b/ROADMAP.adoc @@ -1,22 +1,87 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -= Rsr Template Repo Roadmap +// SPDX-FileCopyrightText: 2025 Hyperpolymath + += rescript-websocket Roadmap +:toc: == Current Status -Initial development phase. +v0.1.0 - Initial release with core WebSocket client and server bindings. == Milestones -=== v0.1.0 - Foundation -* [ ] Core functionality -* [ ] Basic documentation -* [ ] CI/CD pipeline +=== v0.1.0 - Foundation (Current) + +* [x] Core WebSocket client bindings +* [x] Ready state type-safe enum +* [x] Event handlers (open, close, message, error) +* [x] Text and binary message support +* [x] Deno server-side WebSocket upgrade +* [x] Broadcast utilities for multi-client messaging +* [x] Promise-based `waitForOpen` +* [x] JSON send helper +* [x] ReScript interface file (.resi) +* [ ] Basic test suite +* [ ] JSR package publication + +=== v0.2.0 - Enhanced Server + +* [ ] Room/channel abstraction +* [ ] Connection tracking helpers +* [ ] Heartbeat/ping-pong support +* [ ] Connection metadata storage +* [ ] Graceful shutdown with client notification +* [ ] Rate limiting utilities + +=== v0.3.0 - Protocol Support + +* [ ] WebSocket subprotocol helpers +* [ ] JSON-RPC over WebSocket +* [ ] Message framing utilities +* [ ] Compression (permessage-deflate) support +* [ ] Binary message helpers (Blob, TypedArray) + +=== v0.4.0 - Reliability + +* [ ] Automatic reconnection with configurable backoff +* [ ] Connection state machine +* [ ] Message queue for offline buffering +* [ ] Duplicate message detection +* [ ] Acknowledgment patterns === v1.0.0 - Stable Release -* [ ] Full feature set -* [ ] Comprehensive tests -* [ ] Production ready + +* [ ] Comprehensive test coverage (>80%) +* [ ] Performance benchmarks +* [ ] Complete API documentation +* [ ] Migration guide from other libraries +* [ ] Production deployment guide +* [ ] Security audit +* [ ] Stable API guarantee == Future Directions -_To be determined based on community feedback._ +=== Post-1.0 Possibilities + +* **WebTransport support** - When Deno supports WebTransport +* **Server-Sent Events (SSE)** - Companion library for SSE +* **GraphQL subscriptions** - WebSocket transport for GraphQL +* **Cluster support** - Multi-process WebSocket handling +* **Metrics/observability** - OpenTelemetry integration +* **Protocol adapters** - Socket.IO, STOMP, MQTT compatibility + +== Non-Goals + +Things this library intentionally does not aim to provide: + +* Full Socket.IO compatibility layer +* Built-in authentication (use your own middleware) +* Database-backed message persistence +* Horizontal scaling coordination (use Redis/NATS) +* Browser polyfills (standard WebSocket only) + +== Contributing + +See link:CONTRIBUTING.adoc[CONTRIBUTING.adoc] for how to contribute to this roadmap. + +Feature requests and discussions welcome in GitHub Issues. diff --git a/STATE.adoc b/STATE.adoc index e69de29..2220b45 100644 --- a/STATE.adoc +++ b/STATE.adoc @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// SPDX-FileCopyrightText: 2025 Hyperpolymath + += Project State: rescript-websocket + +== Overview + +|=== +|Field |Value + +|Version +|0.1.0 + +|Phase +|Initial Release + +|Status +|Active Development + +|Last Updated +|2025-01 +|=== + +== Components + +=== Client Module + +|=== +|Feature |Status |Notes + +|Basic connection +| Complete +|`make`, `makeWithProtocols` + +|Ready state +| Complete +|Type-safe enum + +|Event handlers +| Complete +|open, close, message, error + +|Send methods +| Complete +|text, binary, JSON + +|Close methods +| Complete +|with/without code + +|Helper functions +| Complete +|isOpen, isConnecting, isClosed, waitForOpen +|=== + +=== Server Module + +|=== +|Feature |Status |Notes + +|WebSocket upgrade +| Complete +|Deno.upgradeWebSocket binding + +|Serve function +| Complete +|with options and handler-only variants + +|Request helpers +| Complete +|requestUrl + +|Handler factory +| Complete +|makeHandler with callbacks + +|Shutdown +| Complete +|Graceful shutdown +|=== + +=== Utilities + +|=== +|Feature |Status |Notes + +|Broadcast +| Complete +|To array of connections + +|Broadcast JSON +| Complete +|JSON serialization included +|=== + +== Current Focus + +* Comprehensive test suite +* JSR package publication +* Documentation refinement + +== Blockers + +None currently. + +== Next Steps + +1. Write test suite for client and server modules +2. Publish to JSR (jsr.io) +3. Add examples directory with runnable demos +4. Room/channel abstraction (v0.2.0) + +== Machine-Readable State + +See `.machine_readable/STATE.scm` for structured state data. diff --git a/examples/README.adoc b/examples/README.adoc new file mode 100644 index 0000000..5f1d77c --- /dev/null +++ b/examples/README.adoc @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// SPDX-FileCopyrightText: 2025 Hyperpolymath + += rescript-websocket Examples + +This directory contains example code demonstrating how to use rescript-websocket. + +== Prerequisites + +Build the examples first: + +[source,bash] +---- +just build +---- + +== Examples + +=== Echo Client + +Simple client that connects to an echo server: + +[source,bash] +---- +deno run --allow-net examples/echo_client.res.js +---- + +=== Chat Server + +Multi-user chat server with broadcasting: + +[source,bash] +---- +# Start the server +deno run --allow-net examples/chat_server.res.js + +# Connect with websocat (in another terminal) +websocat ws://localhost:8080/ws + +# Or open http://localhost:8080 in a browser for instructions +---- + +== Running Examples + +All examples require the `--allow-net` permission for Deno: + +[source,bash] +---- +deno run --allow-net examples/.res.js +---- + +== Source Files + +The `.res` files are ReScript source code. After running `just build`, the compiled `.res.js` files can be executed with Deno. diff --git a/examples/chat_server.res b/examples/chat_server.res new file mode 100644 index 0000000..adf1567 --- /dev/null +++ b/examples/chat_server.res @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// SPDX-FileCopyrightText: 2025 Hyperpolymath + +@@uncurried + +/** + * Chat Server Example + * + * A simple WebSocket chat server that broadcasts messages to all connected clients. + * + * Run: deno run --allow-net examples/chat_server.res.js + * Connect: websocat ws://localhost:8080/ws + */ + +open WebSocket + +// Track all connected clients +let clients: ref> = ref([]) + +// Remove a client from the list +let removeClient = (client: Client.t): unit => { + clients := clients.contents->Array.filter(c => c !== client) +} + +// Create the WebSocket handler +let wsHandler = Server.makeHandler( + ~onConnect=socket => { + let clientCount = clients.contents->Array.length + 1 + Console.log(`Client connected (${clientCount->Int.toString} total)`) + + // Add to clients list + clients := clients.contents->Array.concat([socket]) + + // Send welcome message + socket->Client.send("Welcome to the chat!") + + // Notify others + broadcast( + clients.contents->Array.filter(c => c !== socket), + "[System] A new user has joined the chat", + ) + }, + ~onMessage=(socket, data) => { + Console.log(`Message: ${data}`) + + // Broadcast to all other clients + let others = clients.contents->Array.filter(c => c !== socket) + broadcast(others, data) + + // Echo back to sender with prefix + socket->Client.send(`[You] ${data}`) + }, + ~onClose=(socket, event) => { + Console.log(`Client disconnected: ${event.code->Int.toString}`) + removeClient(socket) + + // Notify others + broadcast(clients.contents, "[System] A user has left the chat") + }, + ~onError=(socket, error) => { + Console.error(`Client error: ${error.message}`) + removeClient(socket) + }, +) + +// HTTP handler that upgrades WebSocket requests +let handler = async (request: Server.request): Fetch.Response.t => { + switch wsHandler(request) { + | Some(response) => response + | None => { + // Regular HTTP response for non-WebSocket requests + let html = ` + + +Chat Server + +

WebSocket Chat Server

+

Connect using a WebSocket client to ws://localhost:8080/ws

+

Or use: websocat ws://localhost:8080/ws

+ +` + Fetch.Response.make(html, ~init={headers: Dict.fromArray([("content-type", "text/html")])}) + } + } +} + +// Start the server +let _ = Server.serve( + { + port: 8080, + hostname: "localhost", + onListen: info => { + Console.log(`Chat server running at http://${info["hostname"]}:${info["port"]->Int.toString}`) + Console.log("WebSocket endpoint: ws://localhost:8080/ws") + }, + }, + handler, +) diff --git a/examples/echo_client.res b/examples/echo_client.res new file mode 100644 index 0000000..89dcda8 --- /dev/null +++ b/examples/echo_client.res @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// SPDX-FileCopyrightText: 2025 Hyperpolymath + +@@uncurried + +/** + * Echo Client Example + * + * Connects to a WebSocket echo server, sends a message, + * and prints the response. + * + * Run: deno run examples/echo_client.res.js + */ + +open WebSocket + +let main = async () => { + let url = "wss://echo.websocket.org" + Console.log(`Connecting to ${url}...`) + + let ws = Client.make(url) + + // Wait for connection + await ws->Client.waitForOpen + Console.log("Connected!") + + // Set up message handler + ws->Client.onMessage(event => { + Console.log(`Received: ${event.data}`) + // Close after receiving echo + ws->Client.closeWithCode(1000, "Done") + }) + + // Set up close handler + ws->Client.onClose(event => { + Console.log(`Connection closed: ${event.code->Int.toString} - ${event.reason}`) + }) + + // Set up error handler + ws->Client.onError(error => { + Console.error(`Error: ${error.message}`) + }) + + // Send a test message + let message = "Hello from ReScript WebSocket!" + Console.log(`Sending: ${message}`) + ws->Client.send(message) +} + +let _ = main() diff --git a/justfile b/justfile index 0e12d43..d5fa22d 100644 --- a/justfile +++ b/justfile @@ -1,9 +1,6 @@ -# RSR-template-repo - RSR Standard Justfile Template +# rescript-websocket - Type-safe WebSocket for ReScript # https://just.systems/man/en/ # -# This is the CANONICAL template for all RSR projects. -# Copy this file to new projects and customize the {{PLACEHOLDER}} values. -# # Run `just` to see all available recipes # Run `just cookbook` to generate docs/just-cookbook.adoc # Run `just combinations` to see matrix recipe options @@ -12,10 +9,10 @@ set shell := ["bash", "-uc"] set dotenv-load := true set positional-arguments := true -# Project metadata - CUSTOMIZE THESE -project := "RSR-template-repo" +# Project metadata +project := "rescript-websocket" version := "0.1.0" -tier := "infrastructure" # 1 | 2 | infrastructure +tier := "2" # Library tier # ═══════════════════════════════════════════════════════════════════════════════ # DEFAULT & HELP @@ -53,28 +50,26 @@ info: # Build the project (debug mode) build *args: @echo "Building {{project}}..." - # TODO: Add build command for your language - # Rust: cargo build {{args}} - # ReScript: npm run build - # Elixir: mix compile + deno task build # Build in release mode with optimizations build-release *args: @echo "Building {{project}} (release)..." - # TODO: Add release build command - # Rust: cargo build --release {{args}} + deno task build # Build and watch for changes build-watch: @echo "Watching for changes..." - # TODO: Add watch command - # Rust: cargo watch -x build - # ReScript: npm run watch + deno task dev + +# Alias for build-watch +dev: build-watch # Clean build artifacts [reversible: rebuild with `just build`] clean: @echo "Cleaning..." - rm -rf target _build dist lib node_modules + deno task clean + rm -rf lib node_modules # Deep clean including caches [reversible: rebuild] clean-all: clean @@ -87,21 +82,23 @@ clean-all: clean # Run all tests test *args: @echo "Running tests..." - # TODO: Add test command - # Rust: cargo test {{args}} - # ReScript: npm test - # Elixir: mix test + deno test --allow-net {{args}} # Run tests with verbose output test-verbose: @echo "Running tests (verbose)..." - # TODO: Add verbose test + deno test --allow-net --trace-leaks # Run tests and generate coverage report test-coverage: @echo "Running tests with coverage..." - # TODO: Add coverage command - # Rust: cargo llvm-cov + deno test --allow-net --coverage=coverage/ + deno coverage coverage/ + +# Watch tests +test-watch: + @echo "Watching tests..." + deno test --allow-net --watch # ═══════════════════════════════════════════════════════════════════════════════ # LINT & FORMAT @@ -110,22 +107,17 @@ test-coverage: # Format all source files [reversible: git checkout] fmt: @echo "Formatting..." - # TODO: Add format command - # Rust: cargo fmt - # ReScript: npm run format - # Elixir: mix format + deno fmt src/ examples/ # Check formatting without changes fmt-check: @echo "Checking format..." - # TODO: Add format check - # Rust: cargo fmt --check + deno fmt --check src/ examples/ # Run linter lint: @echo "Linting..." - # TODO: Add lint command - # Rust: cargo clippy -- -D warnings + deno lint src/ examples/ # Run all quality checks quality: fmt-check lint test @@ -139,23 +131,20 @@ fix: fmt # RUN & EXECUTE # ═══════════════════════════════════════════════════════════════════════════════ -# Run the application -run *args: - @echo "Running {{project}}..." - # TODO: Add run command - # Rust: cargo run {{args}} +# Run the echo client example +run-echo: + @echo "Running echo client example..." + deno run --allow-net examples/echo_client.res.js -# Run in development mode with hot reload -dev: - @echo "Starting dev mode..." - # TODO: Add dev command +# Run the chat server example +run-chat: + @echo "Running chat server example..." + deno run --allow-net examples/chat_server.res.js # Run REPL/interactive mode repl: - @echo "Starting REPL..." - # TODO: Add REPL command - # Elixir: iex -S mix - # Guile: guix shell guile -- guile + @echo "Starting Deno REPL..." + deno repl # ═══════════════════════════════════════════════════════════════════════════════ # DEPENDENCIES @@ -164,16 +153,12 @@ repl: # Install all dependencies deps: @echo "Installing dependencies..." - # TODO: Add deps command - # Rust: (automatic with cargo) - # ReScript: npm install - # Elixir: mix deps.get + deno install # Audit dependencies for vulnerabilities deps-audit: @echo "Auditing dependencies..." - # TODO: Add audit command - # Rust: cargo audit + deno info # ═══════════════════════════════════════════════════════════════════════════════ # DOCUMENTATION diff --git a/rescript.json b/rescript.json index 0100844..a6f83af 100644 --- a/rescript.json +++ b/rescript.json @@ -4,6 +4,10 @@ { "dir": "src", "subdirs": true + }, + { + "dir": "examples", + "subdirs": false } ], "package-specs": [