@nodefony/realtime 10.0.0-beta.6 → 10.0.0-beta.7

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 (62) hide show
  1. package/README.md +5 -0
  2. package/dist/_virtual/{_@oxc-project_runtime@0.157.0 → _@oxc-project_runtime@0.158.0}/helpers/esm/decorate.js +1 -1
  3. package/dist/index.js +15 -3
  4. package/dist/nodefony/decorators/realtimeDecorators.js +231 -39
  5. package/dist/nodefony/src/backplane/RedisBackplane.js +33 -40
  6. package/dist/nodefony/src/backplane/controlOrder.js +34 -5
  7. package/dist/nodefony/src/backplane/envelope.js +422 -45
  8. package/dist/nodefony/src/backplane/ingress.js +93 -0
  9. package/dist/nodefony/src/backplane/presenceMessage.js +205 -0
  10. package/dist/nodefony/src/backplane/publishQueue.js +97 -3
  11. package/dist/nodefony/src/server/RealtimeConnection.js +31 -6
  12. package/dist/nodefony/src/server/RealtimeController.js +87 -44
  13. package/dist/nodefony/src/server/RealtimeHub.js +631 -127
  14. package/dist/nodefony/src/server/admissionBudget.js +8 -8
  15. package/dist/nodefony/src/server/channelPolicyRegistry.js +35 -13
  16. package/dist/nodefony/src/server/channelPolicyResolution.js +191 -0
  17. package/dist/nodefony/src/server/channelPresence.js +488 -0
  18. package/dist/nodefony/src/server/contractRoutes.js +3 -13
  19. package/dist/nodefony/src/server/frameAdmission.js +27 -35
  20. package/dist/nodefony/src/server/presenceBus.js +486 -0
  21. package/dist/nodefony/src/server/realtimeShutdown.js +31 -0
  22. package/dist/nodefony/src/server/remotePresence.js +541 -0
  23. package/dist/nodefony/src/server/subscribedAck.js +3 -0
  24. package/dist/nodefony/src/service/RealtimeService.js +9 -0
  25. package/dist/nodefony/testing/backplaneConformance.js +79 -3
  26. package/dist/nodefony/testing/index.js +22 -8
  27. package/dist/nodefony/testing/presenceClient.js +54 -0
  28. package/dist/types/index.d.ts +6 -1
  29. package/dist/types/nodefony/decorators/realtimeDecorators.d.ts +117 -16
  30. package/dist/types/nodefony/interfaces/IBackplane.d.ts +12 -1
  31. package/dist/types/nodefony/src/backplane/RedisBackplane.d.ts +10 -2
  32. package/dist/types/nodefony/src/backplane/controlOrder.d.ts +23 -0
  33. package/dist/types/nodefony/src/backplane/envelope.d.ts +50 -7
  34. package/dist/types/nodefony/src/backplane/ingress.d.ts +48 -0
  35. package/dist/types/nodefony/src/backplane/presenceMessage.d.ts +150 -0
  36. package/dist/types/nodefony/src/backplane/publishQueue.d.ts +25 -1
  37. package/dist/types/nodefony/src/server/RealtimeConnection.d.ts +3 -0
  38. package/dist/types/nodefony/src/server/RealtimeController.d.ts +19 -1
  39. package/dist/types/nodefony/src/server/RealtimeHub.d.ts +139 -30
  40. package/dist/types/nodefony/src/server/channelPolicyRegistry.d.ts +9 -2
  41. package/dist/types/nodefony/src/server/channelPolicyResolution.d.ts +112 -0
  42. package/dist/types/nodefony/src/server/channelPresence.d.ts +155 -0
  43. package/dist/types/nodefony/src/server/contractRoutes.d.ts +1 -1
  44. package/dist/types/nodefony/src/server/frameAdmission.d.ts +14 -18
  45. package/dist/types/nodefony/src/server/presenceBus.d.ts +112 -0
  46. package/dist/types/nodefony/src/server/realtimeShutdown.d.ts +21 -0
  47. package/dist/types/nodefony/src/server/remotePresence.d.ts +151 -0
  48. package/dist/types/nodefony/src/server/subscribedAck.d.ts +3 -0
  49. package/dist/types/nodefony/src/service/RealtimeService.d.ts +8 -1
  50. package/dist/types/nodefony/testing/backplaneConformance.d.ts +14 -2
  51. package/dist/types/nodefony/testing/index.d.ts +1 -1
  52. package/dist/types/nodefony/testing/presenceClient.d.ts +20 -0
  53. package/docs/actions.md +1 -0
  54. package/docs/architecture.md +21 -6
  55. package/docs/canaux-a-membres.md +13 -3
  56. package/docs/cas-d-usage.md +4 -3
  57. package/docs/configuration.md +38 -2
  58. package/docs/cookbook-chat.md +400 -152
  59. package/docs/observabilite.md +28 -9
  60. package/docs/protocole.md +10 -10
  61. package/docs/securite.md +64 -10
  62. package/package.json +12 -12
@@ -3,7 +3,7 @@ import { AUTHENTICATE_TIMED_OUT } from "./admissionBudget.js";
3
3
  import { WsConnectionTransport } from "../transport/WsConnectionTransport.js";
4
4
  import { getRealtimeHub } from "./RealtimeHub.js";
5
5
  import { ANONYMOUS_REALTIME_TOKEN } from "./AnonymousRealtimeToken.js";
6
- import { DEFAULT_ACTION_POLICY, getRealtimeActions, getRealtimeChannels, getRealtimeInbound } from "../../decorators/realtimeDecorators.js";
6
+ import { getRealtimeActions, getRealtimeChannels, getRealtimeInbound, patternChannelFactory } from "../../decorators/realtimeDecorators.js";
7
7
  import { welcomeEnv } from "./welcomeEnv.js";
8
8
  import { registerControllerPolicies } from "./channelPolicyRegistry.js";
9
9
  import { welcomeIdentityOf } from "./welcomeIdentity.js";
@@ -12,7 +12,7 @@ import { imposedSubscribedAckFrame, subscribedAckFrame } from "./subscribedAck.j
12
12
  import { RealtimeConnection } from "./RealtimeConnection.js";
13
13
  import { deniedDetail, explainsDenials } from "./deniedDetail.js";
14
14
  import { ConnectionAdmission } from "./connectionAdmission.js";
15
- import { admitByRule, gateFrame, releaseRuleAdmission, revokeAdmission } from "./frameAdmission.js";
15
+ import { gateFrame, releaseRuleAdmission, revokeAdmission } from "./frameAdmission.js";
16
16
  import { BootConfigurationError, IDENTITY_DEAD_CLOSE_CODE, JsonRpcError, JsonRpcPeer, JsonRpcServerError, ORIGIN_REFUSED_CLOSE_CODE, PLATFORM_INBOUND, RequestContext, RpcEnvelope, RpcError, Scope, TRY_AGAIN_LATER_CLOSE_CODE, TransportState, identityHint, isPromise, negotiateFrameCodec, stripTrailingSlashes } from "nodefony";
17
17
  import { isWebsocketContext, parseRequestTarget, readBackpressureOptions, toValidationFields } from "@nodefony/http";
18
18
  import { Controller, buildContextParamArgs, getParamArgsMeta, isFrameworkRouter } from "@nodefony/framework";
@@ -23,6 +23,12 @@ import { Controller, buildContextParamArgs, getParamArgsMeta, isFrameworkRouter
23
23
  */
24
24
  const NO_ROUTE_PARAMS = Object.freeze({});
25
25
  /**
26
+ * Préfixe de la ligne `ERROR` écrite quand la poignée de main lève (socket fermée
27
+ * en 1011). Le harnais de test la relit pour citer l'exception : le client, lui,
28
+ * ne reçoit que « internal error » — rien d'interne ne sort par la socket.
29
+ */
30
+ const HANDSHAKE_FAILED_LOG = "WS realtime handshake failed";
31
+ /**
26
32
  * Statut HTTP-équivalent d'une erreur survenue pendant une invocation du pont.
27
33
  *
28
34
  * UNE seule lecture pour les trois sources possibles — la `RpcError` déjà mappée
@@ -44,6 +50,24 @@ function httpStatusOfFrameError(e) {
44
50
  return typeof code === "number" && code >= 400 && code <= 599 ? code : null;
45
51
  }
46
52
  /**
53
+ * L'attente d'un `Retry-After` (RFC 9110 §10.2.3), en secondes : un délai
54
+ * (`120`) tel quel, une date HTTP ramenée à l'écart avec `now` (jamais
55
+ * négatif). Une valeur illisible, ou absente, ne dit rien : `null`.
56
+ *
57
+ * @param value - l'en-tête posé par l'action, s'il l'a été
58
+ * @param now - l'instant de référence d'une date HTTP, en ms (défaut : l'horloge,
59
+ * lue seulement pour une date)
60
+ * @returns les secondes à attendre, ou `null`
61
+ */
62
+ function retryAfterSeconds(value, now) {
63
+ if (value === void 0) return null;
64
+ const text = value.trim();
65
+ if (/^\d+$/u.test(text)) return Number(text);
66
+ if (!/[A-Za-z]/u.test(text)) return null;
67
+ const at = Date.parse(text);
68
+ return Number.isNaN(at) ? null : Math.max(0, Math.ceil((at - (now ?? Date.now())) / 1e3));
69
+ }
70
+ /**
47
71
  * Erreur renvoyée au pair pour une invocation en échec.
48
72
  *
49
73
  * Un refus est une RÉPONSE (statut exposé, symétrie d'un `fetch`) ; une panne
@@ -51,18 +75,18 @@ function httpStatusOfFrameError(e) {
51
75
  * du profil est joint aux refus : le client peut alors radiographier son propre
52
76
  * refus (le 403 `@IsGranted` devient pédagogique au lieu d'être un mur).
53
77
  */
54
- function toFrameRpcError(e, status, requestId) {
78
+ function toFrameRpcError(e, status, requestId, retryAfter = null) {
55
79
  if (e instanceof RpcError) {
56
- if (!requestId) return e;
57
- const data = isRecord(e.data) ? {
58
- ...e.data,
59
- requestId
60
- } : { requestId };
80
+ if (!requestId && retryAfter === null) return e;
81
+ const data = isRecord(e.data) ? { ...e.data } : {};
82
+ if (requestId) data.requestId = requestId;
83
+ if (retryAfter !== null) data.retryAfter = retryAfter;
61
84
  return new RpcError(e.message, e.code, data);
62
85
  }
63
86
  if (status !== null) {
64
87
  const data = { status };
65
88
  if (requestId) data.requestId = requestId;
89
+ if (retryAfter !== null) data.retryAfter = retryAfter;
66
90
  if (status === 422 && e instanceof Error) {
67
91
  const fields = toValidationFields(e);
68
92
  if (fields !== null) data.fields = fields;
@@ -268,7 +292,7 @@ var RealtimeController = class extends Controller {
268
292
  if (ctx === null) throw this.outsideWebsocketError();
269
293
  if (message == null) {
270
294
  this.onHandshake(ctx).catch((e) => {
271
- this.log(`WS realtime handshake failed: ${e instanceof Error ? e.message : String(e)}`, "ERROR");
295
+ this.log(`${HANDSHAKE_FAILED_LOG}: ${e instanceof Error ? e.message : String(e)}`, "ERROR");
272
296
  ctx.connection?.close(1011, "internal error");
273
297
  });
274
298
  return;
@@ -441,18 +465,19 @@ var RealtimeController = class extends Controller {
441
465
  };
442
466
  peerOptions.onFrameAudit = (reason, frame, auditedPeer) => {
443
467
  if (reason !== "denied") return;
444
- this.log("WS realtime frame refused by authorizer", "WARNING");
468
+ const explained = hub.explainDenial(frame, state.peer);
469
+ this.log(explained === null ? "WS realtime frame refused by authorizer" : `WS realtime frame refused by authorizer: ${explained}`, "WARNING");
445
470
  if (!isRecord(frame)) return;
446
471
  const f = frame;
447
472
  if (f.id !== void 0) return;
448
473
  const channel = f.method === "subscribe" ? channelParam(f.params) : typeof f.method === "string" ? f.method : void 0;
449
474
  if (channel !== void 0) {
450
475
  const unadmitted = f.method === channel && state.inbound !== null && Object.hasOwn(state.inbound, channel) && hub.resolveChannelAdmission(channel) !== null;
451
- const hint = unadmitted ? null : identityHint(this.kernel);
476
+ const hint = unadmitted || explained !== null ? null : identityHint(this.kernel);
452
477
  const denied = {
453
478
  channel,
454
479
  reason: "forbidden",
455
- ...deniedDetail(this.kernel?.environment, unadmitted ? `« ${channel} » n'accepte d'entrée que des connexions que sa règle \`allow\` a admises : abonne-toi d'abord au canal (\`subscribe\`), puis pousse une fois l'abonnement acquitté` : `le verrou de frame a refusé l'accès à « ${channel} » pour cette identité : vérifie les rôles ou scopes exigés par le canal (décorateur \`@RealtimeChannel(nom, { roles })\` ou clé \`realtimeChannels\` de la configuration de sécurité), et ceux que porte le jeton — le \`realtime:welcome\` te les rend` + (hint === null ? "" : `. ${hint}`))
480
+ ...deniedDetail(this.kernel?.environment, explained ?? (unadmitted ? `« ${channel} » n'accepte d'entrée que des connexions que sa règle \`allow\` a admises : abonne-toi d'abord au canal (\`subscribe\`), puis pousse une fois l'abonnement acquitté` : `le verrou de frame a refusé l'accès à « ${channel} » pour cette identité : vérifie les rôles ou scopes exigés par le canal (décorateur \`@RealtimeChannel(nom, { roles })\` ou clé \`realtimeChannels\` de la configuration de sécurité), et ceux que porte le jeton — le \`realtime:welcome\` te les rend` + (hint === null ? "" : `. ${hint}`)))
456
481
  };
457
482
  auditedPeer.notify("realtime:denied", denied);
458
483
  }
@@ -475,9 +500,9 @@ var RealtimeController = class extends Controller {
475
500
  };
476
501
  for (const [name, handler] of Object.entries(allActions)) {
477
502
  peer.register(name, handler);
478
- if (!hub.hasExactChannelPolicy(name)) hub.registerChannelPolicy(name, DEFAULT_ACTION_POLICY);
503
+ hub.declareActionDefault(name);
479
504
  }
480
- if (this.realtimeApiRequest()) peer.register("api.request", (params) => this.invokeApiRequest(ctx, params, peer));
505
+ if (this.realtimeApiRequest()) peer.register("api.request", (params) => this.invokeApiRequest(ctx, params, token));
481
506
  this._decoratedChannels = getRealtimeChannels(this);
482
507
  registerControllerPolicies(hub, this.constructor, null);
483
508
  if (!warnedUnenforcedPolicies && hub.hasUnenforcedChannelPolicies()) {
@@ -489,15 +514,7 @@ var RealtimeController = class extends Controller {
489
514
  hub.onPlatformNotice((message, severity) => {
490
515
  noticeSyslog?.log(message, severity, noticeName);
491
516
  });
492
- hub.registerConnection(transport);
493
517
  let revocable = null;
494
- if (typeof token.isValid === "function") {
495
- revocable = {
496
- token,
497
- close: (code, reason) => conn.close(code, reason)
498
- };
499
- hub.registerRevocable(revocable);
500
- }
501
518
  const broadcast = this.realtimeBroadcastChannels();
502
519
  for (const channel of broadcast) hub.markBroadcastChannel(channel);
503
520
  const decoratedInbound = getRealtimeInbound(this);
@@ -544,6 +561,14 @@ var RealtimeController = class extends Controller {
544
561
  peer.dispose("ws closed");
545
562
  this.log("WS realtime client disconnected — cleanup done", "INFO");
546
563
  });
564
+ hub.registerConnection(transport);
565
+ if (typeof token.isValid === "function") {
566
+ revocable = {
567
+ token,
568
+ close: (code, reason) => conn.close(code, reason)
569
+ };
570
+ hub.registerRevocable(revocable);
571
+ }
547
572
  const announcedChannels = [...this._decoratedChannels ? Object.keys(this._decoratedChannels) : [], ...this.realtimeChannels()].filter((channel) => hub.probeChannel(channel, peer));
548
573
  const env = this.kernel?.environment;
549
574
  const welcome = {
@@ -593,12 +618,14 @@ var RealtimeController = class extends Controller {
593
618
  */
594
619
  acknowledgeSubscription(state, channel) {
595
620
  const connection = state.connection;
596
- if (connection?.isImposing(channel) === true) {
597
- state.transport.send(imposedSubscribedAckFrame(channel));
598
- connection.settleImposed(channel, true);
599
- return;
600
- }
601
- state.transport.send(subscribedAckFrame(channel));
621
+ const imposed = connection?.isImposing(channel) === true;
622
+ if (state.binaryCodec !== null) state.peer.notify("realtime:subscribed", imposed ? {
623
+ channel,
624
+ imposed: true
625
+ } : { channel });
626
+ else state.transport.send(imposed ? imposedSubscribedAckFrame(channel) : subscribedAckFrame(channel));
627
+ if (imposed) connection.settleImposed(channel, true);
628
+ if (connection !== null) getRealtimeHub().presenceSubscribed(channel, connection);
602
629
  }
603
630
  /**
604
631
  * Poignée publique de la connexion, créée au premier besoin (abonnement, trame entrante) puis gardée sur son état.
@@ -612,8 +639,13 @@ var RealtimeController = class extends Controller {
612
639
  const peer = state.peer;
613
640
  connection = new RealtimeConnection({
614
641
  impose: (channel) => this.imposeChannel(ctx, channel),
615
- end: (channel, reason) => this.endChannel(ctx, channel, reason)
616
- }, () => getRealtimeHub().getTokenForPeer(peer).getIdentity?.().id ?? null);
642
+ end: (channel, reason) => this.endChannel(ctx, channel, reason),
643
+ send: (frame) => {
644
+ const data = frame.encodeFor(peer.codec);
645
+ if (data !== void 0) state.transport.send(data);
646
+ else if (frame.claimFailure(peer.codec)) this.log("trame partagée non encodable : non envoyée", "ERROR");
647
+ }
648
+ }, () => getRealtimeHub().getTokenForPeer(peer).getIdentity?.() ?? null);
617
649
  state.connection = connection;
618
650
  }
619
651
  return connection;
@@ -674,9 +706,10 @@ var RealtimeController = class extends Controller {
674
706
  leaveChannel(state, channel) {
675
707
  const sink = state.channels.get(channel);
676
708
  if (sink === void 0) return false;
677
- getRealtimeHub().unsubscribe(channel, sink);
709
+ const hub = getRealtimeHub();
710
+ hub.unsubscribe(channel, sink);
678
711
  state.channels.delete(channel);
679
- releaseRuleAdmission(state, channel);
712
+ releaseRuleAdmission(state, channel, hub);
680
713
  return true;
681
714
  }
682
715
  /**
@@ -740,10 +773,7 @@ var RealtimeController = class extends Controller {
740
773
  return;
741
774
  }
742
775
  admission = new ConnectionAdmission({
743
- open: (ch) => {
744
- this.openChannel(ctx, state, ch);
745
- if (state.channels.has(ch)) admitByRule(state, ch);
746
- },
776
+ open: (ch) => this.openChannel(ctx, state, ch),
747
777
  deny: (ch, reason, detail, retryAfterMs) => {
748
778
  this.refuseChannel(state, ch, reason, detail, retryAfterMs);
749
779
  this.log(`WS subscribe refusé (${reason}) → ${ch}`, "DEBUG");
@@ -787,6 +817,12 @@ var RealtimeController = class extends Controller {
787
817
  if (getRealtimeHub().subscribeClient(channel, sink, (ch, publish) => {
788
818
  const decFactory = this._decoratedChannels?.[ch];
789
819
  if (decFactory) return decFactory(ch, publish);
820
+ if (this.createRealtimeChannel === BASE_CREATE_CHANNEL) {
821
+ const byPattern = patternChannelFactory(this, this._decoratedChannels, ch);
822
+ if (byPattern === null) return null;
823
+ servedByPattern = true;
824
+ return byPattern(ch, publish);
825
+ }
790
826
  const dispose = this.createRealtimeChannel(ch, publish);
791
827
  if (dispose !== null) servedByPattern = true;
792
828
  return dispose;
@@ -859,10 +895,11 @@ var RealtimeController = class extends Controller {
859
895
  * mutualisé tant que Ph.6 n'a pas tranché).
860
896
  *
861
897
  * Erreurs → {@link RpcError} (`data.status` = statut HTTP équivalent) :
862
- * `-32602` params invalides, 404 path inconnu, 408 timeout d'action…
898
+ * `-32602` params invalides, 404 path inconnu, 408 timeout d'action… Un
899
+ * `Retry-After` posé par l'action part en `data.retryAfter` (secondes).
863
900
  * Tout autre throw de l'action reste opaque (`-32603`, Zero Trust).
864
901
  */
865
- async invokeApiRequest(ctx, params, peer) {
902
+ async invokeApiRequest(ctx, params, token) {
866
903
  const p = isRecord(params) ? params : void 0;
867
904
  const path = p?.path;
868
905
  if (typeof path !== "string" || path.charCodeAt(0) !== 47) throw new RpcError("api.request: params.path invalide", JsonRpcError.INVALID_PARAMS);
@@ -875,9 +912,8 @@ var RealtimeController = class extends Controller {
875
912
  const pathname = stripTrailingSlashes(target.pathname);
876
913
  const frame = ctx.beginFrame(method, path);
877
914
  let store = null;
915
+ const renderSink = {};
878
916
  try {
879
- const token = getRealtimeHub().getTokenForPeer(peer);
880
- const renderSink = {};
881
917
  const frameStore = {
882
918
  requestId: frame?.requestId ?? ctx.requestId,
883
919
  scheme: ctx.scheme,
@@ -897,7 +933,7 @@ var RealtimeController = class extends Controller {
897
933
  frame?.phaseStart("resolve");
898
934
  let resolver;
899
935
  try {
900
- resolver = router.resolve(ctx, pathname, method === "GET" ? void 0 : method);
936
+ resolver = router.resolve(ctx, pathname, method);
901
937
  } finally {
902
938
  frame?.phaseEnd("resolve");
903
939
  }
@@ -908,6 +944,7 @@ var RealtimeController = class extends Controller {
908
944
  const sp = new URLSearchParams(target.search.slice(1));
909
945
  const query = {};
910
946
  for (const [k, v] of sp) {
947
+ if (Object.hasOwn(Object.prototype, k)) continue;
911
948
  const prev = query[k];
912
949
  if (prev === void 0) query[k] = v;
913
950
  else if (Array.isArray(prev)) prev.push(v);
@@ -931,7 +968,7 @@ var RealtimeController = class extends Controller {
931
968
  frame?.phaseStart("action");
932
969
  try {
933
970
  const { result: actionResult } = await resolver.executeActionGuarded(void 0, true);
934
- const raw = await Promise.resolve(actionResult);
971
+ const raw = await actionResult;
935
972
  if (renderSink.body !== void 0) {
936
973
  const text = Buffer.isBuffer(renderSink.body) ? renderSink.body.toString("utf8") : renderSink.body;
937
974
  let body;
@@ -960,7 +997,7 @@ var RealtimeController = class extends Controller {
960
997
  } catch (e) {
961
998
  const status = httpStatusOfFrameError(e);
962
999
  frame?.finish(status ?? 500, e);
963
- throw toFrameRpcError(e, status, frame?.profiled ? frame.requestId : void 0);
1000
+ throw toFrameRpcError(e, status, frame?.profiled ? frame.requestId : void 0, status === null ? null : retryAfterSeconds(renderSink.retryAfter));
964
1001
  } finally {
965
1002
  if (store !== null) RequestContext.release(store);
966
1003
  ctx.collectFrame(frame);
@@ -980,6 +1017,12 @@ var RealtimeController = class extends Controller {
980
1017
  }
981
1018
  };
982
1019
  /**
1020
+ * La fabrique par défaut de la base (`null` partout) : une classe qui la garde
1021
+ * ne surcharge pas `createRealtimeChannel`, et ses motifs décorés servent alors
1022
+ * les noms qu'ils couvrent. Lue une fois, comparée par référence au `subscribe`.
1023
+ */
1024
+ const BASE_CREATE_CHANNEL = Reflect.get(RealtimeController.prototype, "createRealtimeChannel");
1025
+ /**
983
1026
  * Une frame JSON-RPC porte-t-elle un `id` — réponse ou requête, jamais une
984
1027
  * notification ? Même règle que `isNotification` du cœur (`id` absent), sur une
985
1028
  * frame non typée.
@@ -1043,4 +1086,4 @@ function handshakePath(ctx) {
1043
1086
  return raw.charCodeAt(0) === 47 ? raw : "/";
1044
1087
  }
1045
1088
  }
1046
- export { RealtimeController, RealtimeController as default };
1089
+ export { HANDSHAKE_FAILED_LOG, RealtimeController, RealtimeController as default, retryAfterSeconds };