@marianmeres/ws 0.2.0
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/AGENTS.md +161 -0
- package/API.md +672 -0
- package/LICENSE +21 -0
- package/README.md +211 -0
- package/dist/client/backoff.d.ts +25 -0
- package/dist/client/backoff.js +31 -0
- package/dist/client/heartbeat.d.ts +38 -0
- package/dist/client/heartbeat.js +78 -0
- package/dist/client/outbox.d.ts +65 -0
- package/dist/client/outbox.js +155 -0
- package/dist/client/rooms.d.ts +62 -0
- package/dist/client/rooms.js +120 -0
- package/dist/client/ws-client.d.ts +351 -0
- package/dist/client/ws-client.js +892 -0
- package/dist/mod.d.ts +30 -0
- package/dist/mod.js +29 -0
- package/dist/protocol/constants.d.ts +113 -0
- package/dist/protocol/constants.js +118 -0
- package/dist/protocol/errors.d.ts +92 -0
- package/dist/protocol/errors.js +109 -0
- package/dist/protocol/frames.d.ts +151 -0
- package/dist/protocol/frames.js +12 -0
- package/dist/protocol/mod.d.ts +10 -0
- package/dist/protocol/mod.js +10 -0
- package/dist/protocol.d.ts +9 -0
- package/dist/protocol.js +9 -0
- package/package.json +39 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# @marianmeres/ws — Agent Guide
|
|
2
|
+
|
|
3
|
+
WebSocket client with namespaces, rooms, presence and reconnect, plus a
|
|
4
|
+
demino-mountable reference server.
|
|
5
|
+
|
|
6
|
+
## Quick Reference
|
|
7
|
+
|
|
8
|
+
```yaml
|
|
9
|
+
name: "@marianmeres/ws"
|
|
10
|
+
runtime: "deno (primary), browser/node/bun/workers (client)"
|
|
11
|
+
entry: "./src/mod.ts"
|
|
12
|
+
exports: [".", "./server", "./protocol"]
|
|
13
|
+
test: "deno task test"
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
| Task | File | Key export |
|
|
17
|
+
| ---------------------------- | ------------------------- | ------------------------------------- |
|
|
18
|
+
| Client | `src/client/ws-client.ts` | `createWSClient`, `WSClient` |
|
|
19
|
+
| Reconnect curve | `src/client/backoff.ts` | `backoffDelay` |
|
|
20
|
+
| Outbox + pending acks | `src/client/outbox.ts` | `Outbox` |
|
|
21
|
+
| Room refcounting | `src/client/rooms.ts` | `RoomRegistry` |
|
|
22
|
+
| Liveness probe | `src/client/heartbeat.ts` | `Heartbeat` |
|
|
23
|
+
| Server factory | `src/server/mod.ts` | `createWSApp` |
|
|
24
|
+
| Connections, rooms, delivery | `src/server/service.ts` | `WSService` |
|
|
25
|
+
| Wire definitions | `src/protocol/*` | `ClientFrame`, `ServerFrame`, `CLOSE` |
|
|
26
|
+
|
|
27
|
+
## Project Structure
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
src/
|
|
31
|
+
├── mod.ts # client entry
|
|
32
|
+
├── server.ts # ./server entry -> server/mod.ts
|
|
33
|
+
├── protocol.ts # ./protocol entry -> protocol/mod.ts
|
|
34
|
+
├── protocol/ # dependency-free; imported by BOTH sides
|
|
35
|
+
│ ├── constants.ts # PROTOCOL_VERSION, FRAME, CLOSE, ERROR_CODE, PRESENCE
|
|
36
|
+
│ ├── frames.ts # ClientFrame / ServerFrame unions, WSMessage
|
|
37
|
+
│ └── errors.ts # typed errors
|
|
38
|
+
├── client/
|
|
39
|
+
└── server/
|
|
40
|
+
└── adapters/ # cross-instance fan-out seam (local only today)
|
|
41
|
+
|
|
42
|
+
example/ # standalone reference app — `deno task example`
|
|
43
|
+
├── server.ts # demino apps: /ws (createWSApp), /api, / (static)
|
|
44
|
+
├── history.ts # WSPubSubAdapter decorator -> in-memory backlog
|
|
45
|
+
├── shared.ts # the APPLICATION protocol (what goes in `payload`)
|
|
46
|
+
├── client/ # @marianmeres/vanilla views + store, bundled by
|
|
47
|
+
│ # @marianmeres/deno-build to public/dist/bundle.js
|
|
48
|
+
└── public/ # index.html + design-tokens CSS (theme.css is
|
|
49
|
+
# generated by build-theme.ts and IS committed;
|
|
50
|
+
# dist/ is not)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`src/server.ts` and `src/protocol.ts` exist because the npm build maps subpath
|
|
54
|
+
exports to `src/{name}.ts`. Do not inline them away.
|
|
55
|
+
|
|
56
|
+
## Critical Conventions
|
|
57
|
+
|
|
58
|
+
**Protocol type is not application type.** The protocol owns a closed set of
|
|
59
|
+
frame types; `payload` is opaque and must never be inspected or mutated. The
|
|
60
|
+
predecessor (`stack-sse`) conflated them and had to `delete message.payload.type`
|
|
61
|
+
to compensate — do not reintroduce that.
|
|
62
|
+
|
|
63
|
+
**`protocol/` is the single source of truth.** Both sides import it. Changing a
|
|
64
|
+
frame shape means changing it there, and bumping `PROTOCOL_VERSION` if the
|
|
65
|
+
change is breaking.
|
|
66
|
+
|
|
67
|
+
**Ordering on reconnect is load-bearing.** `#onHello` must re-subscribe _before_
|
|
68
|
+
flushing the outbox, and the server must handle `sub`/`unsub` **synchronously**.
|
|
69
|
+
Together these guarantee a buffered publish cannot land in a room the server has
|
|
70
|
+
not registered yet. There is a test that fails if you break it
|
|
71
|
+
(`buffered publishes flush *after* re-subscribe`).
|
|
72
|
+
|
|
73
|
+
**`sub`/`unsub` bypass the outbox.** They are replayed wholesale by the
|
|
74
|
+
re-subscribe step, so buffering them too would apply them twice.
|
|
75
|
+
|
|
76
|
+
**Every send carries one deadline** spanning queue + flight + ack — not an
|
|
77
|
+
ack-only timeout. Combining acks with infinite retry otherwise produces promises
|
|
78
|
+
that pend forever.
|
|
79
|
+
|
|
80
|
+
**The pong deadline is not restarted by later pings.** It measures time since
|
|
81
|
+
the _oldest_ unanswered ping. Restarting it means any `pingInterval <=
|
|
82
|
+
pongTimeout` silently disables half-open detection. This was a real bug; there
|
|
83
|
+
is a regression test.
|
|
84
|
+
|
|
85
|
+
**Superseded sockets are ignored via `#generation`.** Every socket callback
|
|
86
|
+
checks its captured generation. Without it a slow `onclose` from a dead socket
|
|
87
|
+
cancels the reconnect that replaced it.
|
|
88
|
+
|
|
89
|
+
**Safe defaults are deny.** `allowBroadcast` denies; HTTP injection routes are
|
|
90
|
+
not mounted without `httpAuth`. Do not "helpfully" relax either.
|
|
91
|
+
|
|
92
|
+
**Delivery is at-most-once.** Transmitted-but-unacked sends are never resent.
|
|
93
|
+
If that changes, the server needs deduplication first.
|
|
94
|
+
|
|
95
|
+
## Publishing (JSR + npm)
|
|
96
|
+
|
|
97
|
+
This package publishes to **both** JSR and npm, and JSR's constraints are the
|
|
98
|
+
binding ones. Anything added to a public API surface must satisfy:
|
|
99
|
+
|
|
100
|
+
| Requirement | Rule |
|
|
101
|
+
| --------------------- | ------------------------------------------------------------------------------ |
|
|
102
|
+
| Explicit return types | Every exported function, method and getter — no inferred returns |
|
|
103
|
+
| No slow types | Nothing in a public signature whose type JSR cannot resolve without inference |
|
|
104
|
+
| Module docs | A `/** … @module */` block at the top of **every** file, entry point or not |
|
|
105
|
+
| Symbol docs | Every export **and its members** — fields, methods, getters, ctors, const keys |
|
|
106
|
+
| `@param` / `@returns` | On anything non-obvious; `@throws` wherever a typed error can surface |
|
|
107
|
+
| `@example` | On entry-point modules and the primary factories |
|
|
108
|
+
|
|
109
|
+
Two commands enforce all of it — run both, they check different things:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
deno doc --lint src/mod.ts src/server.ts src/protocol.ts # JSDoc + return types
|
|
113
|
+
deno publish --dry-run --allow-dirty # slow types + packaging
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
`deno doc --lint` is the strict one. Note it does **not** accept a JSDoc block
|
|
117
|
+
containing only tags — `/** @param x - … */` still counts as undocumented. Lead
|
|
118
|
+
with a description sentence, then the tags.
|
|
119
|
+
|
|
120
|
+
Version and publish via `deno task rp` (patch) / `deno task rpm` (minor); both
|
|
121
|
+
run `deno publish` then the npm build.
|
|
122
|
+
|
|
123
|
+
## Before Making Changes
|
|
124
|
+
|
|
125
|
+
1. `deno task test` — 39 tests, all real sockets against a real server
|
|
126
|
+
2. `deno lint && deno fmt --check && deno check src/mod.ts src/server.ts src/protocol.ts`
|
|
127
|
+
3. Touched a public signature? `deno doc --lint src/mod.ts src/server.ts
|
|
128
|
+
src/protocol.ts` **and** `deno publish --dry-run --allow-dirty`
|
|
129
|
+
4. Touching the wire? Update `src/protocol/` first, then both sides
|
|
130
|
+
5. Touching reconnect, outbox or heartbeat? Read `tests/resilience.test.ts`
|
|
131
|
+
first — those tests encode the failure modes the design exists to handle
|
|
132
|
+
6. `deno task npm:build` if packaging changed (npm ships the **client only**;
|
|
133
|
+
the server is Deno-only and excluded via `sourceFiles`) — a new file under
|
|
134
|
+
`src/client/` or `src/protocol/` must be added to `sourceFiles` by hand
|
|
135
|
+
7. Adding or changing a public export? Update `API.md` — it documents **all**
|
|
136
|
+
of them, and that completeness is the contract
|
|
137
|
+
8. Changing the client or server API? Run `deno task example` and click through
|
|
138
|
+
it — the example is the only place the whole stack runs together
|
|
139
|
+
|
|
140
|
+
## Known Gaps
|
|
141
|
+
|
|
142
|
+
- No server-side replay, so no at-least-once delivery
|
|
143
|
+
- `recipients` counts are instance-local; they stay that way under a
|
|
144
|
+
distributed adapter
|
|
145
|
+
- Only `WSPubSubLocal` exists — Redis/Deno-KV adapters are an unimplemented seam
|
|
146
|
+
- Presence is scoped to `(room, namespace)`; broadcast crosses namespaces but
|
|
147
|
+
presence does not
|
|
148
|
+
- Node/Bun cannot run the server (`Deno.upgradeWebSocket`)
|
|
149
|
+
|
|
150
|
+
## Documentation Index
|
|
151
|
+
|
|
152
|
+
| Document | Purpose |
|
|
153
|
+
| ------------------- | ------------------------------------------------ |
|
|
154
|
+
| `README.md` | Human-facing overview and usage |
|
|
155
|
+
| `API.md` | Complete API reference — every public export |
|
|
156
|
+
| `example/README.md` | The reference app: what it demonstrates, and how |
|
|
157
|
+
|
|
158
|
+
`tmp/spec.md` (design spec and decision log) is referenced in some commits but
|
|
159
|
+
is **untracked** — `tmp/*` is gitignored, so it does not exist in a fresh clone.
|
|
160
|
+
The rationale that matters is duplicated as JSDoc on the symbol it explains;
|
|
161
|
+
read that rather than reconstructing it.
|