Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,11 @@ jobs:
# it, and with it the badge.
- name: Run the suite against smocket
run: pnpm test:mock
# The example is documentation that executes, so it rots the moment nothing
# runs it. This builds `dist/` and runs the program, which fails the job if
# the example stops working against the package it demonstrates.
- name: Run the chat room example
run: pnpm example:chat-room

browser:
name: browser (chromium)
Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,13 @@ b.emit('join', 'room-1');
- Multi-client simulation
- Membership cleanup on `disconnect`

## Examples

Runnable programs live in [examples/](examples/), outside the published package.
[chat-room](examples/chat-room/) is one room, two clients, and one broadcast, the
smallest thing that shows an event reaching someone other than the sender. From a
clean checkout, `pnpm install` then `pnpm example:chat-room`.

## Scope

smocket reproduces the delivery and routing layer of Socket.IO. The following are out of scope:
Expand Down
50 changes: 50 additions & 0 deletions examples/chat-room/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Chat room example

One room, two clients, one broadcast. The smallest program that shows an event
reaching someone other than the sender, which is the routing a mock without
[rooms](../../docs/glossary.md#room) cannot reproduce.

## Run it

From the repository root, after `pnpm install`.

```bash
pnpm example:chat-room
```

```
[alice] bob joined
[bob] alice: hello
```

Both lines are fixed, and for two different reasons.

The first one is ordering that the library already guarantees. Connection
completion and every emit are scheduled through one FIFO defer
([0004](../../docs/decisions/0004-connection-deferred-one-tick.md),
[0010](../../docs/decisions/0010-single-defer-primitive-and-fifo.md)), so alice,
created first, also joins first. That first join is broadcast the same way as the
second, with `socket.to(room)`, which reaches the room and skips the sender. The
room holds nobody else at that point, so `alice joined` is sent and received by no
one, which is why the output opens with `bob joined` instead.

The second line is what the acknowledgements are for. Awaiting both joins holds
the message until bob is in the room. Without that, the message would sit in the
queue directly behind alice's own join and reach the server while bob was still
connecting, so the broadcast would find an empty room and the line would be lost.

## What it uses

- `new Server(url)` and `io.on('connection')`, the server entry point socket.io
applications already write against.
- `connect(url, { auth })`, the client side, with the name read back on the
server as `socket.handshake.auth.name`.
- `socket.join(room)` and `socket.to(room).emit(...)`, the
[broadcast](../../docs/glossary.md#broadcast) that reaches the room and skips
the sender.
- `emitWithAck`, which is what makes the last line deterministic rather than
dependent on how far two clients happen to have got when the message is sent.

The server half is the code an application runs against real socket.io. Swapping
a real client for smocket inside a test runner is a separate setup, documented
under `docs/`.
47 changes: 47 additions & 0 deletions examples/chat-room/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// A minimal chat room: two clients in one room, one of them speaks, and the
// other one hears it. The shortest program that shows delivery reaching someone
// other than the sender, which is the part a mock without rooms cannot do.
import { connect, Server } from 'smocket';

const url = 'http://localhost:3000';

// The server side, exactly as it is written against real socket.io. The client
// is `connect` here rather than the `io` alias, so the name `io` stays free for
// the server the way an application would use it.
const io = new Server(url);

io.on('connection', (socket) => {
const name = socket.handshake.auth.name;

socket.on('join', (room, ack) => {
socket.join(room);
// `socket.to(room)` reaches the room and skips this socket, so the joiner is
// not told about its own arrival. The first join therefore reaches nobody,
// the room being empty until it lands.
socket.to(room).emit('system', `${name} joined`);
ack();
});

socket.on('message', (room, text) => {
socket.to(room).emit('message', `${name}: ${text}`);
});
});

// Two clients, each one carrying the name the server reads off the handshake.
const alice = connect(url, { auth: { name: 'alice' } });
const bob = connect(url, { auth: { name: 'bob' } });

for (const [label, client] of [
['alice', alice],
['bob', bob],
]) {
client.on('system', (line) => console.log(`[${label}] ${line}`));
client.on('message', (line) => console.log(`[${label}] ${line}`));
}

// Awaiting both joins is what fixes the last line. bob has to be in the room
// before alice speaks, and without the ack the message would be queued right
// behind alice's own join and arrive while bob was still connecting.
await Promise.all([alice.emitWithAck('join', 'general'), bob.emitWithAck('join', 'general')]);

alice.emit('message', 'general', 'hello');
11 changes: 11 additions & 0 deletions examples/chat-room/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"name": "chat-room-example",
"private": true,
"type": "module",
"scripts": {
"start": "node index.js"
},
"dependencies": {
"smocket": "workspace:*"
}
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
"attw": "attw --pack .",
"assert:no-imports": "node scripts/assert-no-imports.js",
"check:package": "pnpm build && pnpm assert:no-imports && pnpm publint && pnpm attw",
"example:chat-room": "pnpm build && pnpm --filter chat-room-example start",
"conformance": "node scripts/conformance-report.mjs",
"check:conformance": "node scripts/conformance-report.mjs --check"
},
Expand Down
6 changes: 6 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
packages:
- examples/*
allowBuilds:
esbuild: true
onlyBuiltDependencies:
Expand Down