portal-mojo 0.2.10 → 0.2.11

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/docs/realtime.md CHANGED
@@ -17,7 +17,8 @@ disable this shared provider.
17
17
  - An open socket is not ready. The client waits for `auth_required`, then sends
18
18
  `{type:'authenticate',token,prefix:'bearer'}` and becomes ready only after
19
19
  `auth_success`. An auth error closes and suspends the connection until the
20
- token value changes.
20
+ token value changes. The exact direct admission-capacity error described
21
+ below is a transport refusal, not an authentication failure.
21
22
  - Heartbeat is a `ping` every 20 seconds with a 10-second `pong` deadline.
22
23
  A server-initiated `{type:'ping'}` (django-mojo 1.32+ sends one every 20
23
24
  seconds and culls a socket silent for 90) is answered at once with
@@ -25,7 +26,11 @@ disable this shared provider.
25
26
  dispatched to consumers. Answering from the message handler keeps the socket
26
27
  alive while a background tab throttles the client's own ping timer.
27
28
  Reconnect uses capped jittered backoff. Pre-accept close code 4429 enforces at
28
- least 60 seconds, and focus/visibility cannot bypass that deadline.
29
+ least 60 seconds. The direct `{type:'error',message:'Too many connections'}`
30
+ admission refusal while authenticating or ready uses the same delay, even
31
+ when followed by a normal server close. Focus/visibility, token replacement,
32
+ and provider remount cannot bypass the deadline on that client. Queued frames
33
+ from the refused socket are ignored; recovery still requires authentication.
29
34
  - Desired topics are refcounted and replay after authentication. Operations are
30
35
  serialized because acknowledgements carry only the topic and denial is a
31
36
  generic uncorrelated `error`. Denial yields topic status `denied`; it does not
@@ -42,6 +47,12 @@ disable this shared provider.
42
47
 
43
48
  A top-level `{type:'error'}` frame is attributed in order:
44
49
 
50
+ 0. **Exact admission capacity refusal** — `message === 'Too many connections'`
51
+ while authenticating or ready: close and enter bounded `backoff`, without
52
+ dispatching an application error or declaring account access revoked.
53
+ Substrings, the `error` field, and wrapped application events do not match.
54
+ Consumers retain their independent HTTP authorization checks.
55
+
45
56
  1. **During authentication** — auth failure: the client closes and suspends
46
57
  until the token value changes. Surfaces through the lifecycle status only;
47
58
  no `error` event is dispatched.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "portal-mojo",
3
- "version": "0.2.10",
3
+ "version": "0.2.11",
4
4
  "description": "React portal toolkit for django-mojo — typed client + params store, mission-control UI, dependency-free SVG charts, admin section bundles.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -241,6 +241,12 @@ export class RealtimeClient {
241
241
  private open(): void {
242
242
  if (!this.enabled || !this.token || this.suspended || this.socket) return;
243
243
  const run = this.generation;
244
+ // Token refresh and provider remount use this same entry point; neither
245
+ // may bypass a server admission deadline.
246
+ if (this.clock.now() < this.extendedBackoffUntil) {
247
+ this.scheduleReconnect(run, 0);
248
+ return;
249
+ }
244
250
  this.setLifecycle('connecting');
245
251
  let socket: RealtimeSocket;
246
252
  try {
@@ -312,6 +318,21 @@ export class RealtimeClient {
312
318
  if (typeof frame.topic === 'string') this.completeTopic(frame.type, frame.topic);
313
319
  return;
314
320
  }
321
+ if (frame.type === 'error' && frame.message === 'Too many connections'
322
+ && (this.lifecycle === 'authenticating' || this.lifecycle === 'ready')) {
323
+ // django-mojo's admission refusal also arrives after the challenge
324
+ // (or a ready-state capacity check), followed by a normal close.
325
+ // Fence queued frames before closing: late auth_success must not
326
+ // clear the deadline or make this refused socket ready.
327
+ this.extendedBackoffUntil = this.clock.now() + 60_000;
328
+ this.socket = null;
329
+ this.clearHeartbeat();
330
+ this.resetTopicConnectionState();
331
+ this.automaticUserTopic = null;
332
+ this.scheduleReconnect(run, 4429);
333
+ socket.close(4429, 'Connection limit');
334
+ return;
335
+ }
315
336
  if (frame.type === 'error') {
316
337
  if (this.lifecycle === 'authenticating') {
317
338
  this.suspend('auth-failed', 4003, 'Authentication failed');