Pharo Smalltalk micro library for building WebSocket-based APIs easily
# Ripple
A Pharo Smalltalk micro library for building WebSocket-based APIs easily — it's like "Teapot for WebSocket".
Ripple implements a publish/subscribe + request/reply event bus over WebSocket connections, sitting on top of [Teapot](https://github.com/zeroflag/teapot) and [Zinc](https://github.com/svenvc/zinc).
## Overview
Ripple lets you expose server-side Pharo objects to browser clients (or any WebSocket client) through a simple JSON message protocol. Each client identifies itself with a token and can:
- **request** messages to the server and receive a **reply** (correlated by UUID)
- **send** one-way events to the server (no reply)
- **publish** messages to a named address for broadcast delivery
- **register/unregister** subscriptions to addresses
- keep the connection alive with **ping/pong**
See [protocol.md](docs/protocol.md) for the full message protocol reference.
## Installation
Load Ripple into a running Pharo image via Metacello:
```smalltalk
Metacello new
baseline: 'Ripple';
repository: 'github://mumez/Ripple/src';
load.
```
**Dependencies** (loaded automatically):
- [NeoJSON](https://github.com/svenvc/NeoJSON) — JSON serialization
- [Zinc WebSocket](https://github.com/svenvc/zinc) — WebSocket transport
## Quick Start
### 1. Start the server
```smalltalk
RpServer new start.
```
The server binds to `127.0.0.1:8080` by default.
### 2. Customize settings
```smalltalk
| server |
server := RpServer new.
server settings port: 9090.
server settings debugMode: true.
server settings assetsDir: '/path/to/my/assets'.
server start.
```
Settings are backed by environment variables and have sensible defaults:
| Setting | Env var | Default |
|---------|---------|---------|
| `port` | `PHARO_RIPPLE_PORT` | `8080` |
| `debugMode` | `PHARO_RIPPLE_DEBUG_MODE` | `false` |
| `bindAddress` | `PHARO_RIPPLE_BIND_ADDRESS` | `127.0.0.1` |
| `assetsDir` | `PHARO_RIPPLE_ASSETS_DIR` | `<cwd>/assets` |
### 3. Define a handler
Subclass `RpRipple` and override `handleRequest:` and/or `handleSend:`:
```smalltalk
RpRipple subclass: #MyEchoRipple
instanceVariableNames: ''
classVariableNames: ''
package: 'MyApp'.
MyEchoRipple class >> roomName [
^ '/ws/echo'
]
MyEchoRipple >> handleRequest: aMessage [
self webSocket sendReply: aMessage body for: aMessage
]
```
### 4. Register the handler
```smalltalk
| server |
server := RpServer new.
server addRoomOf: MyEchoRipple.
server start.
```
### 5. Connect from the browser
```javascript
const bus = new Ripple('ws://localhost:8080/ws/echo?token=myToken');
bus.connect();
const reply = await bus.send('/greet', { name: 'World' });
console.log(reply);
```
A minimal vanilla-JS client (`Ripple.js`) is included in `test-assets/js/` for testing purposes.
A full client library will be published as a separate repository.
## Architecture
| Class | Role |
|-------|------|
| `RpServer` | HTTP server (Teapot wrapper); registers WebSocket routes |
| `RpWebSocketDelegate` | HTTP → WebSocket protocol upgrade |
| `RpWebSocketResponse` | Preserves original HTTP request through upgrade handshake |
| `RpWebSocket` | ZnWebSocket subclass; adds `sendReply:for:`, `sendPublish:to:`, `sendErrorFor:type:message:`, `sendPong` |
| `RpMessage` | Pure value object parsed from incoming JSON |
| `RpMessageDispatcher` | Routes messages to the correct handler method by type |
| `RpRipple` | Per-session application logic; subclass to implement your handler |
Handler class hierarchy:
```
RpWebSocketBaseHandler Connection registry (token → RpRipple); mutex-protected
└─ RpWebSocketEventBusHandler Pub/sub routing (subscriptionDict); token auto-register on connect
·····>* RpRipple Per-session instance (one per token); subclass per endpoint
```
## Integration Testing
The `Ripple-Core-Tests` package includes a ready-made test handler (`RpTestRipple`) and a browser-based UI (`test-assets/`) that exercises send, request, server publish, and client publish against a live server.
### 1. Make the test UI accessible
`RpServer` serves static files from the `assets/` directory relative to the Pharo image. Copy or symlink `test-assets/` as `assets/` next to your image:
```bash
# symlink (recommended)
ln -s /path/to/Ripple/test-assets /path/to/pharo-image-dir/assets
# or copy
cp -r /path/to/Ripple/test-assets /path/to/pharo-image-dir/assets
```
### 2. Start the server with the test route
`addTestRoute` is provided by `Ripple-Core-Tests` and must be called explicitly — it is not registered by default.
```smalltalk
| server |
server := RpServer new.
server settings allowClientPublish: true. "optional — enables the client publish feature"
server addTestRoute.
server start.
```
### 3. Open the test UI
Open `http://localhost:8080/assets/index.html` in one or more browser tabs.
Each tab connects with a unique session token and can independently trigger:
- **Send** — sends a message to the server; the server echoes it back with the session token
- **Request** — sends a request; the server replies with the session token
- **Start / Stop server publish** — the server broadcasts to all subscribed tabs every 2 seconds (one shared process regardless of how many tabs started it)
- **Publish from client** — requires `allowClientPublish: true` on server settings
## License
MIT