@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.
- package/README.md +5 -0
- package/dist/_virtual/{_@oxc-project_runtime@0.157.0 → _@oxc-project_runtime@0.158.0}/helpers/esm/decorate.js +1 -1
- package/dist/index.js +15 -3
- package/dist/nodefony/decorators/realtimeDecorators.js +231 -39
- package/dist/nodefony/src/backplane/RedisBackplane.js +33 -40
- package/dist/nodefony/src/backplane/controlOrder.js +34 -5
- package/dist/nodefony/src/backplane/envelope.js +422 -45
- package/dist/nodefony/src/backplane/ingress.js +93 -0
- package/dist/nodefony/src/backplane/presenceMessage.js +205 -0
- package/dist/nodefony/src/backplane/publishQueue.js +97 -3
- package/dist/nodefony/src/server/RealtimeConnection.js +31 -6
- package/dist/nodefony/src/server/RealtimeController.js +87 -44
- package/dist/nodefony/src/server/RealtimeHub.js +631 -127
- package/dist/nodefony/src/server/admissionBudget.js +8 -8
- package/dist/nodefony/src/server/channelPolicyRegistry.js +35 -13
- package/dist/nodefony/src/server/channelPolicyResolution.js +191 -0
- package/dist/nodefony/src/server/channelPresence.js +488 -0
- package/dist/nodefony/src/server/contractRoutes.js +3 -13
- package/dist/nodefony/src/server/frameAdmission.js +27 -35
- package/dist/nodefony/src/server/presenceBus.js +486 -0
- package/dist/nodefony/src/server/realtimeShutdown.js +31 -0
- package/dist/nodefony/src/server/remotePresence.js +541 -0
- package/dist/nodefony/src/server/subscribedAck.js +3 -0
- package/dist/nodefony/src/service/RealtimeService.js +9 -0
- package/dist/nodefony/testing/backplaneConformance.js +79 -3
- package/dist/nodefony/testing/index.js +22 -8
- package/dist/nodefony/testing/presenceClient.js +54 -0
- package/dist/types/index.d.ts +6 -1
- package/dist/types/nodefony/decorators/realtimeDecorators.d.ts +117 -16
- package/dist/types/nodefony/interfaces/IBackplane.d.ts +12 -1
- package/dist/types/nodefony/src/backplane/RedisBackplane.d.ts +10 -2
- package/dist/types/nodefony/src/backplane/controlOrder.d.ts +23 -0
- package/dist/types/nodefony/src/backplane/envelope.d.ts +50 -7
- package/dist/types/nodefony/src/backplane/ingress.d.ts +48 -0
- package/dist/types/nodefony/src/backplane/presenceMessage.d.ts +150 -0
- package/dist/types/nodefony/src/backplane/publishQueue.d.ts +25 -1
- package/dist/types/nodefony/src/server/RealtimeConnection.d.ts +3 -0
- package/dist/types/nodefony/src/server/RealtimeController.d.ts +19 -1
- package/dist/types/nodefony/src/server/RealtimeHub.d.ts +139 -30
- package/dist/types/nodefony/src/server/channelPolicyRegistry.d.ts +9 -2
- package/dist/types/nodefony/src/server/channelPolicyResolution.d.ts +112 -0
- package/dist/types/nodefony/src/server/channelPresence.d.ts +155 -0
- package/dist/types/nodefony/src/server/contractRoutes.d.ts +1 -1
- package/dist/types/nodefony/src/server/frameAdmission.d.ts +14 -18
- package/dist/types/nodefony/src/server/presenceBus.d.ts +112 -0
- package/dist/types/nodefony/src/server/realtimeShutdown.d.ts +21 -0
- package/dist/types/nodefony/src/server/remotePresence.d.ts +151 -0
- package/dist/types/nodefony/src/server/subscribedAck.d.ts +3 -0
- package/dist/types/nodefony/src/service/RealtimeService.d.ts +8 -1
- package/dist/types/nodefony/testing/backplaneConformance.d.ts +14 -2
- package/dist/types/nodefony/testing/index.d.ts +1 -1
- package/dist/types/nodefony/testing/presenceClient.d.ts +20 -0
- package/docs/actions.md +1 -0
- package/docs/architecture.md +21 -6
- package/docs/canaux-a-membres.md +13 -3
- package/docs/cas-d-usage.md +4 -3
- package/docs/configuration.md +38 -2
- package/docs/cookbook-chat.md +400 -152
- package/docs/observabilite.md +28 -9
- package/docs/protocole.md +10 -10
- package/docs/securite.md +64 -10
- 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 {
|
|
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 {
|
|
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
|
-
|
|
59
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
503
|
+
hub.declareActionDefault(name);
|
|
479
504
|
}
|
|
480
|
-
if (this.realtimeApiRequest()) peer.register("api.request", (params) => this.invokeApiRequest(ctx, params,
|
|
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
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
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
|
-
|
|
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()
|
|
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,
|
|
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
|
|
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
|
|
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 };
|