@weft-js/realtime-durable-object 0.1.1 → 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 +244 -3
- package/package.json +13 -4
- package/plugin.mjs +4 -1
- package/src/index.ts +41 -11
package/README.md
CHANGED
|
@@ -1,9 +1,250 @@
|
|
|
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-durable-object</h1>
|
|
4
8
|
|
|
5
|
-
|
|
9
|
+
<p align="center">
|
|
10
|
+
<strong>A Cloudflare Durable Object as Weft's realtime bus.</strong><br>
|
|
11
|
+
Live data, channels and shared state across every Durable Object hub on Cloudflare, with no Redis or NATS to run.
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
<p align="center">
|
|
15
|
+
<a href="https://www.npmjs.com/package/@weft-js/realtime-durable-object"><img src="https://img.shields.io/npm/v/@weft-js/realtime-durable-object?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 Cloudflare, Weft's realtime hub runs in many places at once, and a Worker can't hold a socket open past the request
|
|
33
|
+
that started it. This package gives the hubs a meeting point that can: one Durable Object per channel, `WeftBusObject`,
|
|
34
|
+
which holds a WebSocket to every hub listening on that channel and writes each publish to all of them. It's the bus for
|
|
35
|
+
Weft apps deployed with Nitro's `cloudflare_durable` preset.
|
|
36
|
+
|
|
37
|
+
## Highlights
|
|
38
|
+
|
|
39
|
+
- **One object per channel.** Each channel name maps to its own `WeftBusObject` through `idFromName`, so busy channels
|
|
40
|
+
don't share an object.
|
|
41
|
+
- **Hibernation API.** The object accepts sockets with `state.acceptWebSocket`, so Cloudflare can evict it from memory
|
|
42
|
+
between messages without dropping the sockets.
|
|
43
|
+
- **Holds nothing but sockets.** No storage, no history: a publish is a POST that the object writes to every socket it
|
|
44
|
+
holds.
|
|
45
|
+
- **Redials with backoff.** A lost socket is reported to the hub as a gap, then redialled after 500 ms, doubling up to
|
|
46
|
+
30 s, until it's back or the subscription stops.
|
|
47
|
+
- **No runtime imports.** The source imports only types, so workerd runs the file as it is. An app doesn't need
|
|
48
|
+
`@cloudflare/workers-types` for it.
|
|
49
|
+
- **Checked by the build.** `weft build` refuses this bus on any preset other than `cloudflare_durable` (R002), where it
|
|
50
|
+
would silently lose messages.
|
|
51
|
+
- **Tested on workerd.** The tests run the object and two hubs in Miniflare, with no Cloudflare account.
|
|
52
|
+
|
|
53
|
+
## Install
|
|
6
54
|
|
|
7
55
|
```bash
|
|
56
|
+
# npm
|
|
8
57
|
npm install @weft-js/realtime-durable-object
|
|
58
|
+
|
|
59
|
+
# pnpm
|
|
60
|
+
pnpm add @weft-js/realtime-durable-object
|
|
61
|
+
|
|
62
|
+
# bun
|
|
63
|
+
bun add @weft-js/realtime-durable-object
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Or let the `weft` CLI install it and add it to `weft.config.ts` in one step:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
npx weft add realtime-durable-object
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
You also need a Cloudflare account and `wrangler` to deploy. `weft deploy` writes a `wrangler.toml` for the
|
|
74
|
+
`cloudflare` target and builds with `weft build --preset cloudflare_durable`.
|
|
75
|
+
|
|
76
|
+
## Quick start
|
|
77
|
+
|
|
78
|
+
Unlike `@weft-js/realtime-redis` and `@weft-js/realtime-nats`, this bus can't be built from plugin options: it needs a
|
|
79
|
+
Durable Object namespace binding, and a binding only exists at run time. So the plugin in `weft.config.ts` only
|
|
80
|
+
declares the binding, and the bus itself is set in `src/realtime.server.ts`. There are four steps.
|
|
81
|
+
|
|
82
|
+
**1. The plugin.** `weft add` writes this:
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
// weft.config.ts
|
|
86
|
+
import { defineConfig } from '@weft-js/core/config'
|
|
87
|
+
import realtimeDurableObject from
|
|
88
|
+
'@weft-js/realtime-durable-object/plugin'
|
|
89
|
+
|
|
90
|
+
export default defineConfig({
|
|
91
|
+
plugins: [realtimeDurableObject()],
|
|
92
|
+
})
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**2. The bus.** Pass the `WEFT_BUS` binding to `durableObjectBus`:
|
|
96
|
+
|
|
97
|
+
<!-- example: skip reaching `env` depends on how your worker exposes bindings -->
|
|
98
|
+
```ts
|
|
99
|
+
// src/realtime.server.ts
|
|
100
|
+
import { realtime } from '@weft-js/start'
|
|
101
|
+
import { durableObjectBus } from '@weft-js/realtime-durable-object'
|
|
102
|
+
import { env } from 'cloudflare:workers'
|
|
103
|
+
|
|
104
|
+
export default realtime({
|
|
105
|
+
bus: durableObjectBus({ namespace: env.WEFT_BUS }),
|
|
106
|
+
})
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
> [!TIP]
|
|
110
|
+
> `cloudflare:workers` is Cloudflare's module for reading bindings outside a request handler. Any way of reaching the
|
|
111
|
+
> binding works: `durableObjectBus` only needs an object with `idFromName(name)` and `get(id)`.
|
|
112
|
+
|
|
113
|
+
**3. The class.** Export `WeftBusObject` from the worker entry, so Cloudflare can create it:
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
export { WeftBusObject } from '@weft-js/realtime-durable-object'
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**4. The binding and migration.** Add them to `wrangler.toml`:
|
|
120
|
+
|
|
121
|
+
```toml
|
|
122
|
+
[[durable_objects.bindings]]
|
|
123
|
+
name = "WEFT_BUS"
|
|
124
|
+
class_name = "WeftBusObject"
|
|
125
|
+
|
|
126
|
+
[[migrations]]
|
|
127
|
+
tag = "weft-bus-v1"
|
|
128
|
+
new_sqlite_classes = ["WeftBusObject"]
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Then build and deploy:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
npx weft build --preset cloudflare_durable
|
|
135
|
+
cd .output && npx wrangler deploy
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## How it plugs into Weft
|
|
139
|
+
|
|
140
|
+
Weft's realtime hub keeps everything that has state (tags, topics, replay rings, cursors, presence) and hands the bus
|
|
141
|
+
three things to do: publish a string to a channel, subscribe to a channel, and close. That is the `Bus` interface in
|
|
142
|
+
[`@weft-js/start/bus`](https://github.com/Ravonus/weft/blob/main/packages/start/src/bus.ts). The bus is at-most-once
|
|
143
|
+
and ordered per publisher.
|
|
144
|
+
|
|
145
|
+
```text
|
|
146
|
+
publishing hub ──POST message──▶ WeftBusObject for channel "c"
|
|
147
|
+
│
|
|
148
|
+
ws.send
|
|
149
|
+
▼
|
|
150
|
+
every hub subscribed to "c"
|
|
151
|
+
(each holds a WebSocket to the object)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
- **`publish(channel, message)`** gets the channel's object with `namespace.get(namespace.idFromName(channel))` and
|
|
155
|
+
POSTs the message to it. The object answers `204` after writing it to every socket. Any other status rejects the
|
|
156
|
+
publish with `durable object bus: publish answered <status>`.
|
|
157
|
+
- **`subscribe(channel, on, gap)`** asks the same object for a WebSocket (an `upgrade: websocket` request), accepts it,
|
|
158
|
+
and calls `on` for each message. It resolves once the socket is open.
|
|
159
|
+
- **A lost socket** calls `gap`, then redials after `min(30 s, 500 ms × 2^attempt)`. A failed redial is another gap and
|
|
160
|
+
a longer wait. A successful one resets the backoff.
|
|
161
|
+
- **`close()`** stops every subscription this bus opened, closing their sockets with code 1000.
|
|
162
|
+
|
|
163
|
+
### Why it needs `cloudflare_durable`
|
|
164
|
+
|
|
165
|
+
A hub's subscription is a WebSocket that must stay open for as long as the hub listens. A stateless Worker's I/O
|
|
166
|
+
belongs to the request that opened it, so the socket would close with that request. Nitro's `cloudflare_durable`
|
|
167
|
+
preset runs the hub inside a Durable Object, which lives long enough to hold it. On any other preset `weft build`
|
|
168
|
+
fails with R002.
|
|
169
|
+
|
|
170
|
+
`weft deploy` recognises this bus from the source: a `durableObjectBus(` call, an import of
|
|
171
|
+
`@weft-js/realtime-durable-object` or the plugin in `weft.config.ts` counts as a shared bus, so E009 (the memory bus on
|
|
172
|
+
a many-instance host) doesn't fire.
|
|
173
|
+
|
|
174
|
+
### What the plugin declares
|
|
175
|
+
|
|
176
|
+
The plugin provides no slot. It declares the binding the bus reaches and gives agents the setup steps.
|
|
177
|
+
|
|
178
|
+
| Permission | Value |
|
|
179
|
+
| --- | --- |
|
|
180
|
+
| `net` | `binding:WEFT_BUS` |
|
|
181
|
+
|
|
182
|
+
## API reference
|
|
183
|
+
|
|
184
|
+
| Export | From | What it is |
|
|
185
|
+
| --- | --- | --- |
|
|
186
|
+
| `durableObjectBus({ namespace })` | `@weft-js/realtime-durable-object` | The `Bus` for `src/realtime.server.ts`: one `WeftBusObject` per channel, reached through `namespace`. Must be used from a Durable Object hub. |
|
|
187
|
+
| `WeftBusObject` | `@weft-js/realtime-durable-object` | The Durable Object class. Export it from the worker and bind it as `WEFT_BUS` with a `new_sqlite_classes` migration. |
|
|
188
|
+
| `DurableObjectNamespaceLike` | `@weft-js/realtime-durable-object` | Type: the part of a namespace binding the bus uses, `idFromName(name)` and `get(id)`. `env.WEFT_BUS` fits it. |
|
|
189
|
+
| default export, `durableObject` | `@weft-js/realtime-durable-object/plugin` | The plugin for `weft.config.ts`. Takes no options; declares `binding:WEFT_BUS`. |
|
|
190
|
+
|
|
191
|
+
### `durableObjectBus` options
|
|
192
|
+
|
|
193
|
+
| Option | Type | Default | What it does |
|
|
194
|
+
| --- | --- | --- | --- |
|
|
195
|
+
| `namespace` | `DurableObjectNamespaceLike` | required | The Durable Object namespace bound to `WeftBusObject`, usually `env.WEFT_BUS`. |
|
|
196
|
+
|
|
197
|
+
### `WeftBusObject`
|
|
198
|
+
|
|
199
|
+
| Method | What it does |
|
|
200
|
+
| --- | --- |
|
|
201
|
+
| `fetch(request)` | With `upgrade: websocket`: accepts a socket through the hibernation API and answers `101`. Otherwise: reads the body as the message, sends it to every socket, answers `204`. |
|
|
202
|
+
| `webSocketMessage()` | Ignores messages from hubs. Traffic only flows out of the object. |
|
|
203
|
+
| `webSocketClose(ws)` | Closes its end with code 1000. |
|
|
204
|
+
|
|
205
|
+
## Limits
|
|
206
|
+
|
|
207
|
+
- **At-most-once.** The object stores nothing. A hub that was disconnected misses what was published meanwhile, and
|
|
208
|
+
gets a gap instead.
|
|
209
|
+
- **`cloudflare_durable` only.** Not for Node, Bun, Deno or other serverless hosts; use Redis, NATS or `postgresBus`
|
|
210
|
+
there.
|
|
211
|
+
- **One object per channel.** Each object's sockets and request rate are subject to Cloudflare's Durable Object limits.
|
|
212
|
+
- **Messages are strings.** The hub serializes; the bus never parses. A message over 1 MiB is refused by the hub (R008).
|
|
213
|
+
|
|
214
|
+
## Troubleshooting
|
|
215
|
+
|
|
216
|
+
| Symptom | Cause | Fix |
|
|
217
|
+
| --- | --- | --- |
|
|
218
|
+
| `R002` "`durableObjectBus` holds a socket open, and on the `<preset>` preset that socket dies…" | Built for a preset other than `cloudflare_durable` | `weft build --preset cloudflare_durable`. |
|
|
219
|
+
| `durable object bus: publish answered 500` | The object threw, or the binding points at another class | Check the binding's `class_name` is `WeftBusObject` and the class is exported from the worker. |
|
|
220
|
+
| `durable object bus: subscribe answered <status>` | The object didn't upgrade the request | Same as above; a route or middleware in front of the object can also strip the upgrade. |
|
|
221
|
+
| `env.WEFT_BUS` is undefined | The binding is missing from `wrangler.toml`, or has another name | Add the `[[durable_objects.bindings]]` entry named `WEFT_BUS`. |
|
|
222
|
+
| Cloudflare refuses the deploy over the class | No migration creates it | Add a `[[migrations]]` entry with `new_sqlite_classes = ["WeftBusObject"]`. |
|
|
223
|
+
| Pages on one hub miss messages after an object restart | The socket dropped: a gap, then a redial | Expected. What must survive goes through a mutation; clients refetch after a gap. |
|
|
224
|
+
|
|
225
|
+
## Related packages
|
|
226
|
+
|
|
227
|
+
| Package | What it is |
|
|
228
|
+
| --- | --- |
|
|
229
|
+
| [`@weft-js/start`](https://www.npmjs.com/package/@weft-js/start) | The full-stack runtime: the realtime hub, `realtime()`, `memoryBus` and `postgresBus`. |
|
|
230
|
+
| [`@weft-js/realtime-redis`](https://www.npmjs.com/package/@weft-js/realtime-redis) | The `realtime.bus` slot on Redis pub/sub, for server hosts. |
|
|
231
|
+
| [`@weft-js/realtime-nats`](https://www.npmjs.com/package/@weft-js/realtime-nats) | The `realtime.bus` slot on core NATS, for server hosts. |
|
|
232
|
+
| [`@weft-js/multiplayer`](https://www.npmjs.com/package/@weft-js/multiplayer) | Presence, cursors and a relay on `channel`. Its traffic crosses hubs through this bus. |
|
|
233
|
+
| [`@weft-js/cli`](https://www.npmjs.com/package/@weft-js/cli) | `weft build`, `weft deploy` and the R002 and E009 checks. |
|
|
234
|
+
| [`@weft-js/core`](https://www.npmjs.com/package/@weft-js/core) | The compiler and reactive runtime, the Vite plugin, the plugin system and test utilities. |
|
|
235
|
+
|
|
236
|
+
## Contributing
|
|
237
|
+
|
|
238
|
+
See [CONTRIBUTING.md](https://github.com/Ravonus/weft/blob/main/CONTRIBUTING.md). To test this package:
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
pnpm --filter @weft-js/realtime-durable-object test
|
|
9
242
|
```
|
|
243
|
+
|
|
244
|
+
The tests run `WeftBusObject` and two Durable Object hubs on workerd through Miniflare: a message crossing from one hub
|
|
245
|
+
to another, a refused publish, and a lost socket becoming a gap, a redial and a clean `close`. No account or deploy is
|
|
246
|
+
needed.
|
|
247
|
+
|
|
248
|
+
## License
|
|
249
|
+
|
|
250
|
+
[MIT](https://github.com/Ravonus/weft/blob/main/LICENSE)
|
package/package.json
CHANGED
|
@@ -1,16 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@weft-js/realtime-durable-object",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "@weft-js/realtime-durable-object: a Cloudflare Durable Object as the realtime bus, on cloudflare_durable",
|
|
5
5
|
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/Ravonus/weft.git",
|
|
9
|
+
"directory": "packages/realtime-durable-object"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/Ravonus/weft/tree/main/packages/realtime-durable-object#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/Ravonus/weft/issues"
|
|
14
|
+
},
|
|
6
15
|
"type": "module",
|
|
7
16
|
"exports": {
|
|
8
17
|
".": "./src/index.ts",
|
|
9
18
|
"./plugin": "./plugin.mjs"
|
|
10
19
|
},
|
|
11
20
|
"peerDependencies": {
|
|
12
|
-
"@weft-js/start": "^0.1.
|
|
13
|
-
"@weft-js/core": "^0.1.
|
|
21
|
+
"@weft-js/start": "^0.1.3",
|
|
22
|
+
"@weft-js/core": "^0.1.3"
|
|
14
23
|
},
|
|
15
24
|
"engines": {
|
|
16
25
|
"node": ">=26"
|
|
@@ -32,4 +41,4 @@
|
|
|
32
41
|
"publishConfig": {
|
|
33
42
|
"access": "public"
|
|
34
43
|
}
|
|
35
|
-
}
|
|
44
|
+
}
|
package/plugin.mjs
CHANGED
|
@@ -8,7 +8,10 @@ export const durableObject = definePlugin({
|
|
|
8
8
|
weft: '*',
|
|
9
9
|
permissions: { net: ['binding:WEFT_BUS'] },
|
|
10
10
|
contribute: () => ({
|
|
11
|
-
agents: {
|
|
11
|
+
agents: {
|
|
12
|
+
title: 'Realtime bus (Durable Object)',
|
|
13
|
+
text: "Set `bus: durableObjectBus({ namespace: env.WEFT_BUS })` in `src/realtime.server.ts`, and export `WeftBusObject` from the worker with a `new_sqlite_classes` migration. It needs the `cloudflare_durable` preset (R002): a stateless Worker can't hold the socket.",
|
|
14
|
+
},
|
|
12
15
|
}),
|
|
13
16
|
})
|
|
14
17
|
|
package/src/index.ts
CHANGED
|
@@ -8,18 +8,34 @@
|
|
|
8
8
|
import type { Bus } from '@weft-js/start/bus'
|
|
9
9
|
|
|
10
10
|
/** The parts of Workers' API used here, so an app doesn't need `@cloudflare/workers-types` for this file. */
|
|
11
|
-
interface Stub {
|
|
11
|
+
interface Stub {
|
|
12
|
+
fetch(url: string, init?: RequestInit): Promise<Response & { webSocket?: CfSocket | null }>
|
|
13
|
+
}
|
|
12
14
|
/** A Durable Object namespace binding, as far as the bus uses one: `env.WEFT_BUS` fits it. */
|
|
13
|
-
export interface DurableObjectNamespaceLike {
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
export interface DurableObjectNamespaceLike {
|
|
16
|
+
idFromName(name: string): unknown
|
|
17
|
+
get(id: unknown): Stub
|
|
18
|
+
}
|
|
19
|
+
interface CfSocket {
|
|
20
|
+
accept(): void
|
|
21
|
+
send(m: string): void
|
|
22
|
+
close(code?: number): void
|
|
23
|
+
addEventListener(t: string, f: (e: { data?: unknown }) => void): void
|
|
24
|
+
}
|
|
25
|
+
interface CfState {
|
|
26
|
+
acceptWebSocket(ws: CfSocket): void
|
|
27
|
+
getWebSockets(): CfSocket[]
|
|
28
|
+
}
|
|
16
29
|
declare const WebSocketPair: new () => [CfSocket, CfSocket]
|
|
17
30
|
|
|
18
31
|
const BASE = 'https://weft-bus/'
|
|
19
32
|
|
|
20
33
|
/** Export it from the worker entry and bind it: `new_sqlite_classes: ['WeftBusObject']`, binding `WEFT_BUS`. */
|
|
21
34
|
export class WeftBusObject {
|
|
22
|
-
constructor(
|
|
35
|
+
constructor(
|
|
36
|
+
private state: CfState,
|
|
37
|
+
_env?: unknown,
|
|
38
|
+
) {}
|
|
23
39
|
|
|
24
40
|
async fetch(request: Request): Promise<Response> {
|
|
25
41
|
if (request.headers.get('upgrade')?.toLowerCase() === 'websocket') {
|
|
@@ -29,14 +45,18 @@ export class WeftBusObject {
|
|
|
29
45
|
}
|
|
30
46
|
const message = await request.text()
|
|
31
47
|
for (const ws of this.state.getWebSockets()) {
|
|
32
|
-
try {
|
|
48
|
+
try {
|
|
49
|
+
ws.send(message)
|
|
50
|
+
} catch {} // a socket mid-close: its own `gap` covers it
|
|
33
51
|
}
|
|
34
52
|
return new Response(null, { status: 204 })
|
|
35
53
|
}
|
|
36
54
|
|
|
37
55
|
webSocketMessage() {}
|
|
38
56
|
webSocketClose(ws: CfSocket) {
|
|
39
|
-
try {
|
|
57
|
+
try {
|
|
58
|
+
ws.close(1000)
|
|
59
|
+
} catch {}
|
|
40
60
|
}
|
|
41
61
|
}
|
|
42
62
|
|
|
@@ -51,19 +71,27 @@ export function durableObjectBus({ namespace }: { namespace: DurableObjectNamesp
|
|
|
51
71
|
if (!res.ok) throw new Error(`durable object bus: publish answered ${res.status}`)
|
|
52
72
|
},
|
|
53
73
|
async subscribe(channel, on, gap) {
|
|
54
|
-
let stopped = false,
|
|
74
|
+
let stopped = false,
|
|
75
|
+
attempt = 0,
|
|
76
|
+
ws: CfSocket | undefined
|
|
55
77
|
const dial = async (): Promise<void> => {
|
|
56
78
|
const res = await stub(channel).fetch(BASE, { headers: { upgrade: 'websocket' } })
|
|
57
79
|
ws = res.webSocket ?? undefined
|
|
58
80
|
if (!ws) throw new Error(`durable object bus: subscribe answered ${res.status}`)
|
|
59
81
|
ws.accept()
|
|
60
82
|
if (stopped) return ws.close(1000) // stopped while this redial was out
|
|
61
|
-
ws.addEventListener('message', e => on(String(e.data)))
|
|
83
|
+
ws.addEventListener('message', (e) => on(String(e.data)))
|
|
62
84
|
const lost = () => {
|
|
63
85
|
ws = undefined
|
|
64
86
|
if (stopped) return
|
|
65
87
|
gap?.()
|
|
66
|
-
setTimeout(
|
|
88
|
+
setTimeout(
|
|
89
|
+
() =>
|
|
90
|
+
void dial().then(() => {
|
|
91
|
+
attempt = 0
|
|
92
|
+
}, lost),
|
|
93
|
+
Math.min(30_000, 500 * 2 ** attempt++),
|
|
94
|
+
)
|
|
67
95
|
}
|
|
68
96
|
ws.addEventListener('close', lost)
|
|
69
97
|
ws.addEventListener('error', () => {})
|
|
@@ -72,7 +100,9 @@ export function durableObjectBus({ namespace }: { namespace: DurableObjectNamesp
|
|
|
72
100
|
const stop = () => {
|
|
73
101
|
stopped = true
|
|
74
102
|
open.delete(stop)
|
|
75
|
-
try {
|
|
103
|
+
try {
|
|
104
|
+
ws?.close(1000)
|
|
105
|
+
} catch {}
|
|
76
106
|
}
|
|
77
107
|
open.add(stop)
|
|
78
108
|
return stop
|