@ultimat3/realtime 1.0.0 → 1.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.
Files changed (2) hide show
  1. package/package.json +3 -3
  2. package/src/socket.ts +16 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ultimat3/realtime",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "Three-tier realtime: channels, live queries, local-first sync — one protocol, one mutator shape",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -30,7 +30,7 @@
30
30
  "test": "bun test"
31
31
  },
32
32
  "dependencies": {
33
- "@ultimat3/core": "1.0.0",
34
- "@ultimat3/query": "1.0.0"
33
+ "@ultimat3/core": "1.2.0",
34
+ "@ultimat3/query": "1.2.0"
35
35
  }
36
36
  }
package/src/socket.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  // the transport or the change buffer, never here. `sync` is stateless: nothing on this object
5
5
  // survives a restart, and nothing needs to.
6
6
 
7
- import { type Actor, type Clock, systemClock, uuid } from '@ultimat3/core';
7
+ import { type Actor, type Clock, recordConnection, systemClock, uuid } from '@ultimat3/core';
8
8
  import { encode, type Frame } from './sync-protocol';
9
9
 
10
10
  export const CLOSE = {
@@ -162,12 +162,24 @@ export class SocketRegistry {
162
162
  this.#idleTimeoutMs = options.idleTimeoutMs ?? 120_000;
163
163
  }
164
164
 
165
+ /**
166
+ * This package's ONE metrics call site, and it is here rather than in `sync-node.ts` because
167
+ * this map is the only definition of "a live connection on this node": a socket ends on the WS
168
+ * close callback, on the idle sweep, on the drain and on a duplicate id, and every one of those
169
+ * paths already had to come through `add`/`remove`. A gauge moved from the callers instead would
170
+ * leak on whichever path a later change forgets — and a connections gauge that only counts up is
171
+ * an HPA that only scales up.
172
+ */
165
173
  add(socket: SyncSocket): void {
174
+ const replacing = this.#sockets.has(socket.id);
166
175
  this.#sockets.set(socket.id, socket);
176
+ // A re-added id replaces one connection with another; the count did not change.
177
+ if (!replacing) recordConnection(1);
167
178
  }
168
179
 
169
180
  remove(id: string): void {
170
- this.#sockets.delete(id);
181
+ // `Map.delete` answers "was it actually there", so a double close cannot decrement twice.
182
+ if (this.#sockets.delete(id)) recordConnection(-1);
171
183
  }
172
184
 
173
185
  get(id: string): SyncSocket | undefined {
@@ -189,7 +201,8 @@ export class SocketRegistry {
189
201
  for (const socket of this.#sockets.values()) {
190
202
  if (socket.idleFor(now) > this.#idleTimeoutMs) {
191
203
  socket.close(CLOSE.idle, 'idle timeout');
192
- this.#sockets.delete(socket.id);
204
+ // Through `remove`, not the map: the sweep is exactly the abnormal close a gauge leaks on.
205
+ this.remove(socket.id);
193
206
  closed.push(socket);
194
207
  }
195
208
  }