@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 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.