@weft-js/realtime-redis 0.1.0 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +266 -3
- package/package.json +13 -4
- package/plugin.mjs +17 -4
- package/src/index.ts +7 -2
package/README.md
CHANGED
|
@@ -1,9 +1,272 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<a href="https://github.com/Ravonus/weft">
|
|
3
|
+
<img src="https://raw.githubusercontent.com/Ravonus/weft/main/site/public/logo.svg" width="88" height="88" alt="Weft">
|
|
4
|
+
</a>
|
|
5
|
+
</p>
|
|
2
6
|
|
|
3
|
-
>
|
|
7
|
+
<h1 align="center">@weft-js/realtime-redis</h1>
|
|
4
8
|
|
|
5
|
-
|
|
9
|
+
<p align="center">
|
|
10
|
+
<strong>Redis pub/sub as Weft's realtime bus, so live data reaches every instance of your app.</strong><br>
|
|
11
|
+
One line in <code>weft.config.ts</code>, one <code>REDIS_URL</code>, and no change to your pages or server functions.
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
<p align="center">
|
|
15
|
+
<a href="https://www.npmjs.com/package/@weft-js/realtime-redis"><img src="https://img.shields.io/npm/v/@weft-js/realtime-redis?color=145c3c&label=npm" alt="npm version"></a>
|
|
16
|
+
<a href="https://github.com/Ravonus/weft/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-145c3c" alt="MIT license"></a>
|
|
17
|
+
<a href="https://github.com/Ravonus/weft/actions"><img src="https://img.shields.io/github/actions/workflow/status/Ravonus/weft/quality.yml?branch=main&label=checks" alt="checks"></a>
|
|
18
|
+
</p>
|
|
19
|
+
|
|
20
|
+
<p align="center">
|
|
21
|
+
<a href="#quick-start">Quick start</a> ·
|
|
22
|
+
<a href="https://github.com/Ravonus/weft/tree/main/docs">Docs</a> ·
|
|
23
|
+
<a href="https://github.com/Ravonus/weft/tree/main/examples">Examples</a> ·
|
|
24
|
+
<a href="https://github.com/Ravonus/weft/blob/main/CHANGELOG.md">Changelog</a> ·
|
|
25
|
+
<a href="https://github.com/Ravonus/weft">GitHub</a>
|
|
26
|
+
</p>
|
|
27
|
+
|
|
28
|
+
> [!IMPORTANT]
|
|
29
|
+
> **Early release (0.1.x).** APIs can change between minor versions. Pin the version and read the
|
|
30
|
+
> [changelog](https://github.com/Ravonus/weft/blob/main/CHANGELOG.md) before you upgrade.
|
|
31
|
+
|
|
32
|
+
On one server, Weft's realtime needs nothing: messages, `.live()` pokes, channels and shared state go through a memory
|
|
33
|
+
bus inside the process. Run two instances and each one only reaches its own sockets, so a mutation on one never
|
|
34
|
+
refreshes a page connected to the other. This package fills the `realtime.bus` slot with Redis pub/sub, so every
|
|
35
|
+
instance hears every publish. It's for apps that already run Redis, or deploy to a host that runs several instances.
|
|
36
|
+
|
|
37
|
+
## Highlights
|
|
38
|
+
|
|
39
|
+
- **A plugin, not code.** `redis()` in `plugins` replaces the memory bus. Nothing in `src/` changes.
|
|
40
|
+
- **Reads `REDIS_URL`.** The URL comes from the environment unless you pass `url`, so no secret lands in the config.
|
|
41
|
+
- **Several apps, one server.** Every channel name is prefixed (`weft:` by default), so apps can share a Redis.
|
|
42
|
+
- **Lazy.** It opens no connection until the first publish or subscribe, and `weft build` never connects.
|
|
43
|
+
- **Reconnects on its own.** ioredis retries forever and resubscribes. Each reconnect after the first is reported to
|
|
44
|
+
the hub as a gap, so clients know that messages may be missing.
|
|
45
|
+
- **Tested against a real server.** The test suite runs the shared `realtime.bus@1` conformance checks against a
|
|
46
|
+
`redis-server` it starts, then kills and restarts, to prove the gap and the resume.
|
|
47
|
+
|
|
48
|
+
## Install
|
|
6
49
|
|
|
7
50
|
```bash
|
|
51
|
+
# npm
|
|
8
52
|
npm install @weft-js/realtime-redis
|
|
53
|
+
|
|
54
|
+
# pnpm
|
|
55
|
+
pnpm add @weft-js/realtime-redis
|
|
56
|
+
|
|
57
|
+
# bun
|
|
58
|
+
bun add @weft-js/realtime-redis
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Or let the `weft` CLI install it and add it to `weft.config.ts` in one step:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
npx weft add redis
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`weft add` installs the package and adds the plugin to `weft.config.ts`.
|
|
68
|
+
|
|
69
|
+
## Quick start
|
|
70
|
+
|
|
71
|
+
1. Run a Redis server. For local work:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
docker run --rm -p 6379:6379 redis
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
2. Add the plugin. `weft add redis` writes this for you:
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
// weft.config.ts
|
|
81
|
+
import { defineConfig } from '@weft-js/core/config'
|
|
82
|
+
import realtimeRedis from '@weft-js/realtime-redis/plugin'
|
|
83
|
+
|
|
84
|
+
export default defineConfig({
|
|
85
|
+
plugins: [realtimeRedis()],
|
|
86
|
+
})
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`import { redis } from '@weft-js/realtime-redis/plugin'` is the same function under its own name.
|
|
90
|
+
|
|
91
|
+
3. Set the URL in the app's environment:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
# .env
|
|
95
|
+
REDIS_URL=redis://localhost:6379
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
4. Start two instances on different ports and open a page that reads a query with `.live()` on each. A mutation through
|
|
99
|
+
one refreshes the page on the other.
|
|
100
|
+
|
|
101
|
+
## How it plugs into Weft
|
|
102
|
+
|
|
103
|
+
Weft's realtime hub keeps everything that has state (tags, topics, replay rings, cursors, presence) and hands the bus
|
|
104
|
+
three things to do: publish a string to a channel, subscribe to a channel, and close. That is the `Bus` interface in
|
|
105
|
+
[`@weft-js/start/bus`](https://github.com/Ravonus/weft/blob/main/packages/start/src/bus.ts). A bus is at-most-once and
|
|
106
|
+
ordered per publisher, like this one.
|
|
107
|
+
|
|
108
|
+
The plugin provides the `realtime.bus` slot (version 1, one provider, server side). At build time Weft writes the slot
|
|
109
|
+
binding to `.weft/slots/realtime.bus.ts`, and the server entry wraps it in a bus that loads this package on the first
|
|
110
|
+
publish or subscribe. The order of precedence is:
|
|
111
|
+
|
|
112
|
+
| Where the bus comes from | Wins over |
|
|
113
|
+
| --- | --- |
|
|
114
|
+
| `bus` in `src/realtime.server.ts` | everything: an explicit choice beats a plugin |
|
|
115
|
+
| a `realtime.bus` plugin in `weft.config.ts` (this one, or `@weft-js/realtime-nats`) | the memory bus |
|
|
116
|
+
| nothing | the memory bus, one process only |
|
|
117
|
+
|
|
118
|
+
So if `src/realtime.server.ts` already sets `bus: postgresBus(...)`, adding this plugin changes nothing until you take
|
|
119
|
+
that line out.
|
|
120
|
+
|
|
121
|
+
### Using it without the plugin
|
|
122
|
+
|
|
123
|
+
`redisBus()` is a plain function, so you can set it in `src/realtime.server.ts` instead, next to the other realtime
|
|
124
|
+
options:
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
// src/realtime.server.ts
|
|
128
|
+
import { realtime } from '@weft-js/start'
|
|
129
|
+
import { redisBus } from '@weft-js/realtime-redis'
|
|
130
|
+
|
|
131
|
+
export default realtime({
|
|
132
|
+
bus: redisBus({
|
|
133
|
+
url: process.env.REDIS_URL ?? '',
|
|
134
|
+
prefix: 'shop:',
|
|
135
|
+
}),
|
|
136
|
+
origins: ['https://admin.example.com'],
|
|
137
|
+
})
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Use this form when you want the bus next to `origins`, `limits` or `changes`, or when you build the URL from other
|
|
141
|
+
settings. The package still has to be installed.
|
|
142
|
+
|
|
143
|
+
## Options
|
|
144
|
+
|
|
145
|
+
`redis(options)` in `weft.config.ts` and `redisBus(options)` take the same options. The plugin passes them through to
|
|
146
|
+
`bus()`, which fills `url` from `REDIS_URL` when it's missing.
|
|
147
|
+
|
|
148
|
+
| Option | Type | Default | What it does |
|
|
149
|
+
| --- | --- | --- | --- |
|
|
150
|
+
| `url` | `string` | `process.env.REDIS_URL` (plugin and `bus()` only) | Server to connect to. Must start with `redis://` or `rediss://` (TLS). Required for `redisBus()`. |
|
|
151
|
+
| `prefix` | `string` | `'weft:'` | Put in front of every Redis channel name. Give each app its own when apps share a server. |
|
|
152
|
+
|
|
153
|
+
The plugin's options end up in generated code, so keep them to plain values. Leave the password in `REDIS_URL` rather
|
|
154
|
+
than in `url`.
|
|
155
|
+
|
|
156
|
+
### Environment
|
|
157
|
+
|
|
158
|
+
| Variable | Read by | Needed |
|
|
159
|
+
| --- | --- | --- |
|
|
160
|
+
| `REDIS_URL` | the plugin and `bus()` | At run time, unless `url` is given. `redis://user:pass@host:6379/0` or `rediss://…` for TLS. |
|
|
161
|
+
| `WEFT_BUILD` | `redisBus()` | Set by `weft build`. While it's set a missing URL is not an error, since a build loads the module but never connects. |
|
|
162
|
+
|
|
163
|
+
### What the plugin declares
|
|
164
|
+
|
|
165
|
+
`weft plugins` and the trust check show what a plugin may reach. This one declares:
|
|
166
|
+
|
|
167
|
+
| Permission | Value |
|
|
168
|
+
| --- | --- |
|
|
169
|
+
| `net` | `env:REDIS_URL`, the host in that variable |
|
|
170
|
+
| `env` | `REDIS_URL`, `WEFT_BUILD` |
|
|
171
|
+
|
|
172
|
+
## How it behaves
|
|
173
|
+
|
|
174
|
+
- **Two connections.** A Redis connection in subscribe mode can't publish, so the bus opens one for publishing and one
|
|
175
|
+
for subscribing, each on first use.
|
|
176
|
+
- **One Redis subscription per channel.** Several listeners on the same channel in one process share a single
|
|
177
|
+
`SUBSCRIBE`. The bus unsubscribes from Redis when the last one stops.
|
|
178
|
+
- **Errors don't crash the process.** Connection errors are swallowed and ioredis keeps retrying
|
|
179
|
+
(`maxRetriesPerRequest: null`), because an unhandled `error` event would end Node.
|
|
180
|
+
- **A reconnect is a gap.** Redis pub/sub keeps nothing for a subscriber that was away. Every `ready` after the first
|
|
181
|
+
calls the hub's `gap`, and the hub treats everything before it as outside the resume window. Clients refetch rather
|
|
182
|
+
than trust a stream with a hole in it.
|
|
183
|
+
- **`close()`** disconnects both connections. The next publish or subscribe connects again.
|
|
184
|
+
|
|
185
|
+
## API reference
|
|
186
|
+
|
|
187
|
+
| Export | From | What it is |
|
|
188
|
+
| --- | --- | --- |
|
|
189
|
+
| `redisBus(options)` | `@weft-js/realtime-redis` | A `Bus` over Redis pub/sub, connecting on first use. Throws Q013 without a `redis://` or `rediss://` URL, except during a build. |
|
|
190
|
+
| `bus(options?)` | `@weft-js/realtime-redis` | The `realtime.bus` provider the plugin loads: `redisBus` with `url` defaulting to `REDIS_URL`. |
|
|
191
|
+
| `RedisBusOptions` | `@weft-js/realtime-redis` | Type: `{ url: string; prefix?: string }`. |
|
|
192
|
+
| default export, `redis` | `@weft-js/realtime-redis/plugin` | The plugin for `weft.config.ts`. Takes the same options as `bus()`. |
|
|
193
|
+
|
|
194
|
+
The returned bus has `name: 'redis'`, `publish(channel, message)`, `subscribe(channel, on, gap?)` (resolves once Redis
|
|
195
|
+
confirms the subscription, with a function that stops it) and `close()`.
|
|
196
|
+
|
|
197
|
+
## Recipes
|
|
198
|
+
|
|
199
|
+
### Several apps on one Redis
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
// weft.config.ts
|
|
203
|
+
import { defineConfig } from '@weft-js/core/config'
|
|
204
|
+
import { redis } from '@weft-js/realtime-redis/plugin'
|
|
205
|
+
|
|
206
|
+
export default defineConfig({
|
|
207
|
+
plugins: [redis({ prefix: 'billing:' })],
|
|
208
|
+
})
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### TLS on a managed Redis
|
|
212
|
+
|
|
213
|
+
Most hosted Redis services give a `rediss://` URL. Use it as it is:
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
REDIS_URL=rediss://default:secret@my-redis.example.com:6380
|
|
9
217
|
```
|
|
218
|
+
|
|
219
|
+
### Covering a gap in your own channel
|
|
220
|
+
|
|
221
|
+
The bus is at-most-once, and a reconnect can drop messages. Anything that must survive goes through a mutation, and a
|
|
222
|
+
channel's `collect` plus a history query fills in what a client missed. The
|
|
223
|
+
[live data guide](https://github.com/Ravonus/weft/blob/main/site/content/learn/live-data.md) shows the pattern.
|
|
224
|
+
|
|
225
|
+
## Limits
|
|
226
|
+
|
|
227
|
+
- **At-most-once.** Redis pub/sub has no history. A message published while an instance is disconnected is gone for
|
|
228
|
+
that instance; it gets a gap instead.
|
|
229
|
+
- **Plain pub/sub only.** It doesn't use Redis Streams, keyspace notifications or sharded pub/sub (`SPUBLISH`).
|
|
230
|
+
- **Messages are strings.** The hub serializes before publishing; the bus never parses.
|
|
231
|
+
- **Sizes are the hub's.** A message over 1 MiB is refused by the hub (R008) before it reaches Redis.
|
|
232
|
+
|
|
233
|
+
## Troubleshooting
|
|
234
|
+
|
|
235
|
+
| Symptom | Cause | Fix |
|
|
236
|
+
| --- | --- | --- |
|
|
237
|
+
| `Q013` "`redisBus` needs a `redis://` URL" at startup | `REDIS_URL` is unset, or the URL has another scheme (`http://`, a bare host) | Set `REDIS_URL` to a `redis://` or `rediss://` URL, or pass `url`. Credentials are masked in the message. |
|
|
238
|
+
| `R002` from `weft build` | The preset runs many instances and the app is still on the memory bus | Add this plugin, `@weft-js/realtime-nats`, or `postgresBus` in `src/realtime.server.ts`. |
|
|
239
|
+
| `E009` from `weft deploy` | The same problem, found for a deploy target | As above. |
|
|
240
|
+
| The plugin is listed but nothing crosses instances | `src/realtime.server.ts` sets its own `bus`, which wins | Remove that `bus`, or set `redisBus()` there. |
|
|
241
|
+
| Messages published while Redis was down never arrived | Pub/sub keeps nothing; the reconnect was reported as a gap | Expected. Put what must survive in a mutation, and refetch after a gap. |
|
|
242
|
+
| Two apps see each other's messages | Same server, same `prefix` | Give each app its own `prefix`. |
|
|
243
|
+
|
|
244
|
+
## Related packages
|
|
245
|
+
|
|
246
|
+
| Package | What it is |
|
|
247
|
+
| --- | --- |
|
|
248
|
+
| [`@weft-js/start`](https://www.npmjs.com/package/@weft-js/start) | The full-stack runtime: the realtime hub, `realtime()`, `memoryBus` and `postgresBus` (`@weft-js/start/bus/postgres`). |
|
|
249
|
+
| [`@weft-js/realtime-nats`](https://www.npmjs.com/package/@weft-js/realtime-nats) | The same slot on core NATS. |
|
|
250
|
+
| [`@weft-js/realtime-durable-object`](https://www.npmjs.com/package/@weft-js/realtime-durable-object) | The bus for Cloudflare, on Durable Objects. |
|
|
251
|
+
| [`@weft-js/multiplayer`](https://www.npmjs.com/package/@weft-js/multiplayer) | Presence, cursors and a relay on `channel`. Its traffic crosses instances through this bus. |
|
|
252
|
+
| [`@weft-js/core`](https://www.npmjs.com/package/@weft-js/core) | The plugin system (`definePlugin`, slots) and `busSuite`, the conformance checks a bus must pass. |
|
|
253
|
+
|
|
254
|
+
## Contributing
|
|
255
|
+
|
|
256
|
+
See [CONTRIBUTING.md](https://github.com/Ravonus/weft/blob/main/CONTRIBUTING.md). To test this package:
|
|
257
|
+
|
|
258
|
+
```bash
|
|
259
|
+
pnpm --filter @weft-js/realtime-redis test
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
The test against a real server needs a `redis-server` binary. Name it in `WEFT_TEST_REDIS`; without it that test is
|
|
263
|
+
skipped and says so:
|
|
264
|
+
|
|
265
|
+
```bash
|
|
266
|
+
WEFT_TEST_REDIS=$(command -v redis-server) \
|
|
267
|
+
pnpm --filter @weft-js/realtime-redis test
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
## License
|
|
271
|
+
|
|
272
|
+
[MIT](https://github.com/Ravonus/weft/blob/main/LICENSE)
|
package/package.json
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@weft-js/realtime-redis",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "@weft-js/realtime-redis: the realtime.bus provider for Redis pub/sub",
|
|
5
5
|
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/Ravonus/weft.git",
|
|
9
|
+
"directory": "packages/realtime-redis"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/Ravonus/weft/tree/main/packages/realtime-redis#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/Ravonus/weft/issues"
|
|
14
|
+
},
|
|
6
15
|
"type": "module",
|
|
7
16
|
"exports": {
|
|
8
17
|
".": "./src/index.ts",
|
|
@@ -12,8 +21,8 @@
|
|
|
12
21
|
"ioredis": "6.0.0"
|
|
13
22
|
},
|
|
14
23
|
"peerDependencies": {
|
|
15
|
-
"@weft-js/start": "^0.1.
|
|
16
|
-
"@weft-js/core": "^0.1.
|
|
24
|
+
"@weft-js/start": "^0.1.3",
|
|
25
|
+
"@weft-js/core": "^0.1.3"
|
|
17
26
|
},
|
|
18
27
|
"engines": {
|
|
19
28
|
"node": ">=26"
|
|
@@ -31,4 +40,4 @@
|
|
|
31
40
|
"publishConfig": {
|
|
32
41
|
"access": "public"
|
|
33
42
|
}
|
|
34
|
-
}
|
|
43
|
+
}
|
package/plugin.mjs
CHANGED
|
@@ -9,10 +9,23 @@ export const redis = definePlugin({
|
|
|
9
9
|
version: '0.1.0',
|
|
10
10
|
weft: '*',
|
|
11
11
|
permissions: { net: ['env:REDIS_URL'], env: ['REDIS_URL', 'WEFT_BUILD'] },
|
|
12
|
-
contribute: options => ({
|
|
13
|
-
provides: [
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
contribute: (options) => ({
|
|
13
|
+
provides: [
|
|
14
|
+
provide(slot, {
|
|
15
|
+
sides: { server: { module: '@weft-js/realtime-redis', export: 'bus', options } },
|
|
16
|
+
}),
|
|
17
|
+
],
|
|
18
|
+
agents: {
|
|
19
|
+
title: 'Realtime bus (Redis)',
|
|
20
|
+
text: '`realtime.bus` is Redis, at `REDIS_URL`. Rooms, shared state, `.live` and streams reach every instance. It is at-most-once: anything that must survive is a mutation, and `collect` plus a query covers a gap.',
|
|
21
|
+
},
|
|
22
|
+
map: [
|
|
23
|
+
{
|
|
24
|
+
want: 'Change the realtime bus',
|
|
25
|
+
call: 'replace this plugin in `plugins`, or set `bus` in `src/realtime.server.ts`',
|
|
26
|
+
have: 'Redis',
|
|
27
|
+
},
|
|
28
|
+
],
|
|
16
29
|
}),
|
|
17
30
|
})
|
|
18
31
|
|
package/src/index.ts
CHANGED
|
@@ -17,7 +17,11 @@ export interface RedisBusOptions {
|
|
|
17
17
|
export function redisBus({ url, prefix = 'weft:' }: RedisBusOptions): Bus {
|
|
18
18
|
if (!url && process.env.WEFT_BUILD) url = 'redis://localhost' // a build loads this module and never connects
|
|
19
19
|
if (!/^rediss?:\/\//.test(url ?? ''))
|
|
20
|
-
throw new StartError(
|
|
20
|
+
throw new StartError(
|
|
21
|
+
'Q013',
|
|
22
|
+
`\`redisBus\` needs a \`redis://\` URL, not \`${(url ?? '').replace(/\/\/.*@/, '//…@')}\``,
|
|
23
|
+
'set the URL, or leave `bus` out for the memory bus on one instance',
|
|
24
|
+
)
|
|
21
25
|
let pub: Redis | undefined, sub: Redis | undefined
|
|
22
26
|
const listeners = new Map<string, Set<(message: string) => void>>()
|
|
23
27
|
const gaps = new Set<() => void>()
|
|
@@ -73,4 +77,5 @@ export function redisBus({ url, prefix = 'weft:' }: RedisBusOptions): Bus {
|
|
|
73
77
|
}
|
|
74
78
|
|
|
75
79
|
/** The `realtime.bus` provider: `REDIS_URL` unless the plugin was given a `url`. */
|
|
76
|
-
export const bus = (options: Partial<RedisBusOptions> = {}) =>
|
|
80
|
+
export const bus = (options: Partial<RedisBusOptions> = {}) =>
|
|
81
|
+
redisBus({ ...options, url: options.url ?? process.env.REDIS_URL ?? '' })
|