Ripple

Description

Pharo Smalltalk micro library for building WebSocket-based APIs easily

Details

Source
GitHub
Dialect
pharo (25% confidence)
Created
May 28, 2026
Updated
June 13, 2026
Topics
pharo-smalltalk websocket

Categories

Networking

README excerpt

# 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
← Back to results