@rlanz/socket 0.0.1-6 → 0.0.1-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 (63) hide show
  1. package/README.md +164 -149
  2. package/build/{types-DiYaFvgi.d.ts → base_channel-Dr40l7CW.d.ts} +174 -169
  3. package/build/chunk-2X6WGCH5.js +120 -0
  4. package/build/chunk-2X6WGCH5.js.map +1 -0
  5. package/build/chunk-6PJ4ALJD.js +115 -0
  6. package/build/chunk-6PJ4ALJD.js.map +1 -0
  7. package/build/chunk-E6ILCRL3.js +182 -0
  8. package/build/chunk-E6ILCRL3.js.map +1 -0
  9. package/build/{chunk-SHH6U4CI.js → chunk-G75J233Z.js} +1 -1
  10. package/build/chunk-G75J233Z.js.map +1 -0
  11. package/build/{chunk-B4Y3TNDI.js → chunk-IUQ3GYZZ.js} +5 -24
  12. package/build/chunk-IUQ3GYZZ.js.map +1 -0
  13. package/build/chunk-QNOOQXYY.js +899 -0
  14. package/build/chunk-QNOOQXYY.js.map +1 -0
  15. package/build/{chunk-FVPY6HZW.js → chunk-YVC7IMDN.js} +21 -5
  16. package/build/chunk-YVC7IMDN.js.map +1 -0
  17. package/build/framework-B-4kGmAw.d.ts +13 -0
  18. package/build/index-Fg-tkHhP.d.ts +150 -0
  19. package/build/index.d.ts +23 -12
  20. package/build/index.js +12 -14
  21. package/build/index.js.map +1 -1
  22. package/build/providers/socket_provider.d.ts +2 -2
  23. package/build/providers/socket_provider.js +468 -215
  24. package/build/providers/socket_provider.js.map +1 -1
  25. package/build/services/socket.d.ts +2 -2
  26. package/build/{socket_service-D3jKrleE.d.ts → socket_service-BYEB8LIN.d.ts} +7 -7
  27. package/build/src/assembler_hook.js +33 -59
  28. package/build/src/assembler_hook.js.map +1 -1
  29. package/build/src/client/index.d.ts +3 -147
  30. package/build/src/client/index.js +4 -837
  31. package/build/src/client/index.js.map +1 -1
  32. package/build/src/client/react.d.ts +13 -5
  33. package/build/src/client/react.js +27 -11
  34. package/build/src/client/react.js.map +1 -1
  35. package/build/src/client/types.d.ts +30 -36
  36. package/build/src/client/vue.d.ts +17 -6
  37. package/build/src/client/vue.js +31 -13
  38. package/build/src/client/vue.js.map +1 -1
  39. package/build/src/decorators.d.ts +3 -2
  40. package/build/src/decorators.js +1 -1
  41. package/build/src/health_check.d.ts +2 -2
  42. package/build/src/health_check.js +14 -3
  43. package/build/src/health_check.js.map +1 -1
  44. package/build/src/otel.js +0 -1
  45. package/build/src/otel.js.map +1 -1
  46. package/build/src/testing.d.ts +3 -12
  47. package/build/src/testing.js +1 -1
  48. package/build/src/types/tracing_channels.d.ts +1 -2
  49. package/build/src/types.d.ts +1 -1
  50. package/package.json +2 -9
  51. package/build/chunk-B4Y3TNDI.js.map +0 -1
  52. package/build/chunk-FVPY6HZW.js.map +0 -1
  53. package/build/chunk-HK7Z65DA.js +0 -19
  54. package/build/chunk-HK7Z65DA.js.map +0 -1
  55. package/build/chunk-SFAY2ZA4.js +0 -28
  56. package/build/chunk-SFAY2ZA4.js.map +0 -1
  57. package/build/chunk-SHH6U4CI.js.map +0 -1
  58. package/build/chunk-YAX5EHHB.js +0 -453
  59. package/build/chunk-YAX5EHHB.js.map +0 -1
  60. package/build/framework-DuW6zpPk.d.ts +0 -14
  61. package/build/src/client/svelte.d.ts +0 -23
  62. package/build/src/client/svelte.js +0 -82
  63. package/build/src/client/svelte.js.map +0 -1
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  BaseChannel,
3
- ChannelRouter,
4
- PRESENCE_DATA_KEY,
5
- PresenceManager,
6
3
  SocketResponseError
7
- } from "../chunk-YAX5EHHB.js";
8
- import "../chunk-SHH6U4CI.js";
4
+ } from "../chunk-2X6WGCH5.js";
5
+ import {
6
+ ChannelPatternSyntax
7
+ } from "../chunk-E6ILCRL3.js";
8
+ import "../chunk-G75J233Z.js";
9
9
  import {
10
10
  broadcastChannel,
11
11
  channelMessageChannel,
@@ -16,7 +16,7 @@ import {
16
16
  } from "../chunk-D3HUBCBW.js";
17
17
  import {
18
18
  SocketFake
19
- } from "../chunk-B4Y3TNDI.js";
19
+ } from "../chunk-IUQ3GYZZ.js";
20
20
 
21
21
  // providers/socket_provider.ts
22
22
  import { ServerResponse } from "http";
@@ -579,15 +579,255 @@ var MessageQueue = class {
579
579
  }
580
580
  };
581
581
 
582
- // src/broadcasting.ts
583
- function userRoom(userId) {
584
- return `socket:user:${String(userId)}`;
585
- }
586
-
587
582
  // src/socket_bus.ts
588
583
  import { randomUUID } from "crypto";
589
584
  import { Bus } from "@boringnode/bus";
590
585
 
586
+ // src/presence_manager.ts
587
+ var PRESENCE_DATA_KEY = "__rlanzSocketPresence";
588
+ var PresenceManager = class {
589
+ #presence = /* @__PURE__ */ new Map();
590
+ #fetchSockets = null;
591
+ /**
592
+ * Sets the socket fetcher used to build distributed snapshots.
593
+ */
594
+ setSocketFetcher(fetchSockets) {
595
+ this.#fetchSockets = fetchSockets;
596
+ }
597
+ /**
598
+ * Adds a user to a channel.
599
+ */
600
+ join(channel, socket, member) {
601
+ const user = {
602
+ ...member.data,
603
+ id: member.id,
604
+ joinedAt: /* @__PURE__ */ new Date()
605
+ };
606
+ const users = this.#presence.get(channel) ?? /* @__PURE__ */ new Map();
607
+ users.set(socket.id, user);
608
+ this.#presence.set(channel, users);
609
+ this.#setSocketPresence(socket, channel, user);
610
+ return user;
611
+ }
612
+ /**
613
+ * Removes a user from a channel.
614
+ */
615
+ leave(channel, socket) {
616
+ const channelPresence = this.#presence.get(channel);
617
+ if (channelPresence) {
618
+ channelPresence.delete(socket.id);
619
+ if (channelPresence.size === 0) {
620
+ this.#presence.delete(channel);
621
+ }
622
+ }
623
+ this.#deleteSocketPresence(socket, channel);
624
+ }
625
+ /**
626
+ * Removes a user from every channel.
627
+ * Returns the list of channels the user left.
628
+ */
629
+ leaveAll(socket) {
630
+ const leftChannels = [];
631
+ for (const [channel, users] of this.#presence) {
632
+ if (users.has(socket.id)) {
633
+ users.delete(socket.id);
634
+ leftChannels.push(channel);
635
+ if (users.size === 0) {
636
+ this.#presence.delete(channel);
637
+ }
638
+ this.#deleteSocketPresence(socket, channel);
639
+ }
640
+ }
641
+ return leftChannels;
642
+ }
643
+ /**
644
+ * Gets presence data for a channel.
645
+ */
646
+ async snapshot(channel) {
647
+ if (this.#fetchSockets) {
648
+ return this.#getFromSockets(channel);
649
+ }
650
+ return this.#localSnapshot(channel);
651
+ }
652
+ /**
653
+ * Gets the number of users in a channel.
654
+ */
655
+ async count(channel) {
656
+ return (await this.snapshot(channel)).count;
657
+ }
658
+ /**
659
+ * Checks whether a socket is locally present in a channel.
660
+ */
661
+ hasLocal(channel, socketId) {
662
+ return this.#presence.get(channel)?.has(socketId) || false;
663
+ }
664
+ /**
665
+ * Gets the presence user retained for one local socket.
666
+ */
667
+ getLocalUser(channel, socketId) {
668
+ return this.#presence.get(channel)?.get(socketId);
669
+ }
670
+ /**
671
+ * Lists every local channel where a socket is present.
672
+ */
673
+ getLocalChannelsForSocket(socketId) {
674
+ const channels = [];
675
+ for (const [channel, users] of this.#presence) {
676
+ if (users.has(socketId)) {
677
+ channels.push(channel);
678
+ }
679
+ }
680
+ return channels;
681
+ }
682
+ /**
683
+ * Lists sockets that are locally present in a channel.
684
+ */
685
+ getLocalSockets(channel) {
686
+ const users = this.#presence.get(channel);
687
+ if (!users) {
688
+ return [];
689
+ }
690
+ return Array.from(users.entries()).map(([socketId, user]) => ({
691
+ id: socketId,
692
+ data: {
693
+ [PRESENCE_DATA_KEY]: {
694
+ [channel]: user
695
+ }
696
+ }
697
+ }));
698
+ }
699
+ #localSnapshot(channel) {
700
+ return this.#createSnapshot(channel, Array.from(this.#presence.get(channel)?.entries() ?? []));
701
+ }
702
+ async #getFromSockets(channel) {
703
+ const sockets = await this.#fetchSockets(channel);
704
+ const users = sockets.flatMap((socket) => {
705
+ const user = socket.data?.[PRESENCE_DATA_KEY]?.[channel];
706
+ return user ? [[socket.id, user]] : [];
707
+ });
708
+ return this.#createSnapshot(channel, users);
709
+ }
710
+ #createSnapshot(channel, connections) {
711
+ const usersById = /* @__PURE__ */ new Map();
712
+ for (const connection of connections) {
713
+ const current = usersById.get(connection[1].id);
714
+ if (!current || this.#compareConnections(connection, current) < 0) {
715
+ usersById.set(connection[1].id, connection);
716
+ }
717
+ }
718
+ const users = [...usersById.values()].sort((left, right) => this.#compareConnections(left, right)).map(([, user]) => user);
719
+ return {
720
+ channel,
721
+ users,
722
+ count: users.length
723
+ };
724
+ }
725
+ #compareConnections([leftSocketId, leftUser], [rightSocketId, rightUser]) {
726
+ const joinedAtDifference = leftUser.joinedAt.getTime() - rightUser.joinedAt.getTime();
727
+ return joinedAtDifference || leftSocketId.localeCompare(rightSocketId);
728
+ }
729
+ #setSocketPresence(socket, channel, user) {
730
+ socket.data[PRESENCE_DATA_KEY] ??= {};
731
+ socket.data[PRESENCE_DATA_KEY][channel] = user;
732
+ }
733
+ #deleteSocketPresence(socket, channel) {
734
+ if (!socket.data[PRESENCE_DATA_KEY]) {
735
+ return;
736
+ }
737
+ delete socket.data[PRESENCE_DATA_KEY][channel];
738
+ if (Object.keys(socket.data[PRESENCE_DATA_KEY]).length === 0) {
739
+ delete socket.data[PRESENCE_DATA_KEY];
740
+ }
741
+ }
742
+ };
743
+
744
+ // src/presence_socket_frame.ts
745
+ var PresenceSocketFrame = class _PresenceSocketFrame {
746
+ constructor(transportValue, socketValue) {
747
+ this.transportValue = transportValue;
748
+ this.socketValue = socketValue;
749
+ }
750
+ /**
751
+ * Create one transport frame from a local socket presence entry.
752
+ */
753
+ static fromSocket(channel, socket) {
754
+ const user = socket.data[PRESENCE_DATA_KEY]?.[channel];
755
+ if (!user || Number.isNaN(user.joinedAt.getTime())) {
756
+ throw new Error(`Socket "${socket.id}" has no valid presence for channel "${channel}"`);
757
+ }
758
+ const transport = {
759
+ id: socket.id,
760
+ data: {
761
+ [PRESENCE_DATA_KEY]: {
762
+ [channel]: {
763
+ // Keep custom fields. Convert only the presence timestamp.
764
+ ...user,
765
+ joinedAt: user.joinedAt.toISOString()
766
+ }
767
+ }
768
+ }
769
+ };
770
+ return new _PresenceSocketFrame(transport, socket);
771
+ }
772
+ /**
773
+ * Validate one transport value and restore its presence timestamps.
774
+ */
775
+ static fromTransport(value) {
776
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
777
+ return null;
778
+ }
779
+ const transport = value;
780
+ if (typeof transport.id !== "string" || !transport.data || typeof transport.data !== "object" || Array.isArray(transport.data)) {
781
+ return null;
782
+ }
783
+ const data = transport.data;
784
+ const presence = data[PRESENCE_DATA_KEY];
785
+ if (presence === void 0) {
786
+ return new _PresenceSocketFrame(transport, {
787
+ id: transport.id,
788
+ data: { ...data }
789
+ });
790
+ }
791
+ if (!presence || typeof presence !== "object" || Array.isArray(presence)) {
792
+ return null;
793
+ }
794
+ const restoredPresence = {};
795
+ for (const [channel, candidate] of Object.entries(presence)) {
796
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) {
797
+ return null;
798
+ }
799
+ const user = candidate;
800
+ if (typeof user.id !== "string" || typeof user.joinedAt !== "string" || Number.isNaN(Date.parse(user.joinedAt))) {
801
+ return null;
802
+ }
803
+ restoredPresence[channel] = {
804
+ ...user,
805
+ id: user.id,
806
+ joinedAt: new Date(user.joinedAt)
807
+ };
808
+ }
809
+ return new _PresenceSocketFrame(transport, {
810
+ id: transport.id,
811
+ data: {
812
+ ...data,
813
+ [PRESENCE_DATA_KEY]: restoredPresence
814
+ }
815
+ });
816
+ }
817
+ /**
818
+ * Return the validated transport value.
819
+ */
820
+ toTransport() {
821
+ return this.transportValue;
822
+ }
823
+ /**
824
+ * Return the socket value with restored presence timestamps.
825
+ */
826
+ toSocket() {
827
+ return this.socketValue;
828
+ }
829
+ };
830
+
591
831
  // src/duration.ts
592
832
  import { parse as parseDurationExpression } from "@lukeed/ms";
593
833
  function parseDuration(name, duration) {
@@ -604,27 +844,17 @@ function parseDuration(name, duration) {
604
844
  // src/socket_bus.ts
605
845
  var DEFAULT_CHANNEL = "socket::broadcast";
606
846
  var DEFAULT_PRESENCE_TIMEOUT = 100;
607
- function parsePresenceSocket(value) {
608
- if (!value || typeof value !== "object" || Array.isArray(value)) {
609
- return null;
610
- }
611
- const socket = value;
612
- if (typeof socket.id !== "string" || !socket.data || typeof socket.data !== "object" || Array.isArray(socket.data)) {
613
- return null;
614
- }
615
- const presence = socket.data[PRESENCE_DATA_KEY];
616
- if (presence === void 0) {
617
- return socket;
618
- }
619
- if (!presence || typeof presence !== "object" || Array.isArray(presence)) {
620
- return null;
621
- }
622
- for (const user of Object.values(presence)) {
623
- if (!user || typeof user !== "object" || typeof user.id !== "string" || typeof user.name !== "string" || typeof user.joinedAt !== "string" || Number.isNaN(Date.parse(user.joinedAt))) {
624
- return null;
625
- }
847
+ var DEFAULT_RETRY_QUEUE_MAX_SIZE = 1e3;
848
+ function resolveRetryQueueConfig(config) {
849
+ const maxSize = config?.maxSize === void 0 ? DEFAULT_RETRY_QUEUE_MAX_SIZE : config.maxSize;
850
+ if (maxSize !== null && (!Number.isSafeInteger(maxSize) || maxSize <= 0)) {
851
+ throw new Error("transport.retryQueue.maxSize must be a positive integer or null");
626
852
  }
627
- return socket;
853
+ return {
854
+ enabled: true,
855
+ maxSize,
856
+ ...config
857
+ };
628
858
  }
629
859
  function parseBusMessage(payload) {
630
860
  if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
@@ -637,8 +867,6 @@ function parseBusMessage(payload) {
637
867
  switch (message.type) {
638
868
  case "channel:event":
639
869
  return typeof message.channel === "string" && typeof message.event === "string" && Array.isArray(message.except) && message.except.every((socketId) => typeof socketId === "string") ? message : null;
640
- case "user:event":
641
- return typeof message.room === "string" && typeof message.event === "string" ? message : null;
642
870
  case "broadcast:event":
643
871
  return typeof message.event === "string" ? message : null;
644
872
  case "presence:sockets:request":
@@ -647,7 +875,7 @@ function parseBusMessage(payload) {
647
875
  if (typeof message.target !== "string" || typeof message.requestId !== "string" || !Array.isArray(message.sockets)) {
648
876
  return null;
649
877
  }
650
- const sockets = Array.from(message.sockets, parsePresenceSocket);
878
+ const sockets = Array.from(message.sockets, PresenceSocketFrame.fromTransport);
651
879
  return sockets.every((socket) => socket !== null) ? { ...message, sockets } : null;
652
880
  default:
653
881
  return null;
@@ -658,7 +886,9 @@ var SocketBus = class {
658
886
  this.handlers = handlers;
659
887
  this.#channel = transport.channel ?? DEFAULT_CHANNEL;
660
888
  this.#presenceTimeout = parseDuration("transport.presenceTimeout", transport.presenceTimeout) ?? DEFAULT_PRESENCE_TIMEOUT;
661
- this.#bus = new Bus(transport.driver(), { retryQueue: { enabled: true } });
889
+ this.#bus = new Bus(transport.driver(), {
890
+ retryQueue: resolveRetryQueueConfig(transport.retryQueue)
891
+ });
662
892
  }
663
893
  #origin = randomUUID();
664
894
  #channel;
@@ -676,9 +906,6 @@ var SocketBus = class {
676
906
  case "channel:event":
677
907
  this.handlers.channel(message);
678
908
  return;
679
- case "user:event":
680
- this.handlers.user(message);
681
- return;
682
909
  case "broadcast:event":
683
910
  this.handlers.broadcast(message);
684
911
  return;
@@ -706,16 +933,6 @@ var SocketBus = class {
706
933
  };
707
934
  void this.#bus.publish(this.#channel, message);
708
935
  }
709
- publishUser(room, event, data) {
710
- const message = {
711
- type: "user:event",
712
- origin: this.#origin,
713
- room,
714
- event,
715
- data
716
- };
717
- void this.#bus.publish(this.#channel, message);
718
- }
719
936
  publishBroadcast(event, data) {
720
937
  const message = {
721
938
  type: "broadcast:event",
@@ -734,7 +951,7 @@ var SocketBus = class {
734
951
  return;
735
952
  }
736
953
  this.#pendingPresenceRequests.delete(requestId);
737
- resolve(request.sockets.map((socket) => this.#deserializePresenceSocket(socket)));
954
+ resolve(request.sockets.map((socket) => socket.toSocket()));
738
955
  }, this.#presenceTimeout);
739
956
  this.#pendingPresenceRequests.set(requestId, {
740
957
  sockets: [],
@@ -753,7 +970,7 @@ var SocketBus = class {
753
970
  async close() {
754
971
  for (const [requestId, request] of this.#pendingPresenceRequests) {
755
972
  clearTimeout(request.timeout);
756
- request.resolve(request.sockets.map((socket) => this.#deserializePresenceSocket(socket)));
973
+ request.resolve(request.sockets.map((socket) => socket.toSocket()));
757
974
  this.#pendingPresenceRequests.delete(requestId);
758
975
  }
759
976
  await this.#bus.disconnect();
@@ -778,23 +995,6 @@ var SocketBus = class {
778
995
  }
779
996
  request.sockets.push(...message.sockets);
780
997
  }
781
- #deserializePresenceSocket(socket) {
782
- const presence = socket.data[PRESENCE_DATA_KEY];
783
- return {
784
- id: socket.id,
785
- data: {
786
- [PRESENCE_DATA_KEY]: presence ? Object.fromEntries(
787
- Object.entries(presence).map(([channel, user]) => [
788
- channel,
789
- {
790
- ...user,
791
- joinedAt: new Date(user.joinedAt)
792
- }
793
- ])
794
- ) : void 0
795
- }
796
- };
797
- }
798
998
  };
799
999
 
800
1000
  // src/socket_upgrader.ts
@@ -807,25 +1007,23 @@ var SocketUpgrader = class _SocketUpgrader {
807
1007
  this.runWithHttpContext = runWithHttpContext;
808
1008
  this.reportError = reportError;
809
1009
  this.#path = config?.path ?? DEFAULT_WEBSOCKET_PATH;
810
- this.#allowedOrigins = new Set((config?.allowedOrigins ?? []).map(_SocketUpgrader.#parseOrigin));
811
1010
  }
812
1011
  #path;
813
- #allowedOrigins;
814
1012
  async handle(request, socket, head, accept) {
815
1013
  if (!this.#matchesPath(request)) {
816
1014
  return false;
817
1015
  }
818
- if (!this.#isOriginAllowed(request)) {
819
- _SocketUpgrader.reject(socket, 403, "Forbidden");
820
- return true;
821
- }
822
- const accepted = await this.#authenticate(request);
823
- if (!accepted) {
1016
+ const result = await this.#prepareUpgrade(request);
1017
+ if ("rejected" in result) {
1018
+ if (result.rejected === "origin") {
1019
+ _SocketUpgrader.reject(socket, 403, "Forbidden");
1020
+ return true;
1021
+ }
824
1022
  _SocketUpgrader.reject(socket, 401, "Unauthorized");
825
1023
  return true;
826
1024
  }
827
1025
  this.server.handleUpgrade(request, socket, head, (connection) => {
828
- accept(connection, request, accepted);
1026
+ accept(connection, request, result.accepted);
829
1027
  });
830
1028
  return true;
831
1029
  }
@@ -837,35 +1035,17 @@ var SocketUpgrader = class _SocketUpgrader {
837
1035
  }
838
1036
  socket.destroy();
839
1037
  }
840
- static #parseOrigin(value) {
841
- let url;
842
- try {
843
- url = new URL(value);
844
- } catch {
845
- throw new Error(`websocket.allowedOrigins contains an invalid origin: ${value}`);
846
- }
847
- if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.pathname !== "/" || url.search || url.hash) {
848
- throw new Error(`websocket.allowedOrigins contains an invalid origin: ${value}`);
849
- }
850
- return url.origin;
851
- }
852
1038
  #matchesPath(request) {
853
1039
  const url = new URL(request.url ?? "/", "ws://localhost");
854
1040
  return url.pathname === this.#path;
855
1041
  }
856
- #isOriginAllowed(request) {
1042
+ #isOriginAllowed(request, httpContext) {
857
1043
  const origin = request.headers.origin;
858
1044
  if (!origin) {
859
1045
  return true;
860
1046
  }
861
- let parsedOrigin;
862
- try {
863
- parsedOrigin = _SocketUpgrader.#parseOrigin(origin);
864
- } catch {
865
- return false;
866
- }
867
- if (this.#allowedOrigins.has(parsedOrigin)) {
868
- return true;
1047
+ if (this.config?.origin !== void 0) {
1048
+ return this.#matchesOriginPolicy(origin, httpContext);
869
1049
  }
870
1050
  const host = request.headers.host;
871
1051
  if (!host) {
@@ -875,37 +1055,63 @@ var SocketUpgrader = class _SocketUpgrader {
875
1055
  const forwardedProtocol = (Array.isArray(forwardedHeader) ? forwardedHeader[0] : forwardedHeader)?.split(",")[0]?.trim();
876
1056
  const protocol = forwardedProtocol === "http" || forwardedProtocol === "https" ? forwardedProtocol : request.socket.encrypted ? "https" : "http";
877
1057
  try {
878
- return parsedOrigin === new URL(`${protocol}://${host}`).origin;
1058
+ return origin === new URL(`${protocol}://${host}`).origin;
879
1059
  } catch {
880
1060
  return false;
881
1061
  }
882
1062
  }
883
- async #authenticate(request) {
1063
+ #matchesOriginPolicy(origin, httpContext) {
1064
+ let originPolicy = this.config?.origin ?? false;
1065
+ if (typeof originPolicy === "function") {
1066
+ originPolicy = originPolicy(origin, httpContext);
1067
+ }
1068
+ if (originPolicy === true || originPolicy === "*") {
1069
+ return true;
1070
+ }
1071
+ if (originPolicy === false) {
1072
+ return false;
1073
+ }
1074
+ if (Array.isArray(originPolicy)) {
1075
+ return originPolicy.includes(origin);
1076
+ }
1077
+ return originPolicy.split(",").includes(origin);
1078
+ }
1079
+ async #prepareUpgrade(request) {
1080
+ return this.runWithHttpContext(
1081
+ request,
1082
+ async (httpContext) => {
1083
+ const accepted = await this.#authenticate(httpContext);
1084
+ return accepted ? { accepted } : { rejected: "authentication" };
1085
+ },
1086
+ (httpContext) => {
1087
+ return this.#isOriginAllowed(request, httpContext) ? void 0 : { rejected: "origin" };
1088
+ }
1089
+ );
1090
+ }
1091
+ async #authenticate(httpContext) {
884
1092
  const config = this.config;
885
- return this.runWithHttpContext(request, async (httpContext) => {
886
- if (!config?.authenticate) {
887
- return { httpContext };
1093
+ if (!config?.authenticate) {
1094
+ return { httpContext };
1095
+ }
1096
+ try {
1097
+ const ctx = {
1098
+ httpContext
1099
+ };
1100
+ const result = await config.authenticate(ctx);
1101
+ if (result === false || result === null || result === void 0) {
1102
+ return null;
888
1103
  }
1104
+ return {
1105
+ httpContext,
1106
+ user: result
1107
+ };
1108
+ } catch (error) {
889
1109
  try {
890
- const ctx = {
891
- httpContext
892
- };
893
- const result = await config.authenticate(ctx);
894
- if (result === false || result === null || result === void 0) {
895
- return null;
896
- }
897
- return {
898
- httpContext,
899
- user: result
900
- };
901
- } catch (error) {
902
- try {
903
- this.reportError("socket authentication failed: %s", error);
904
- } catch {
905
- }
906
- return null;
1110
+ this.reportError("socket authentication failed: %s", error);
1111
+ } catch {
907
1112
  }
908
- });
1113
+ return null;
1114
+ }
909
1115
  }
910
1116
  };
911
1117
 
@@ -1030,8 +1236,6 @@ var SocketService = class extends Emittery {
1030
1236
  #httpServer = null;
1031
1237
  #upgradeHandler = null;
1032
1238
  #sockets = /* @__PURE__ */ new Map();
1033
- #userRooms = /* @__PURE__ */ new Map();
1034
- #socketUserRoom = /* @__PURE__ */ new Map();
1035
1239
  #channelSubscriptions = null;
1036
1240
  #presenceManager = null;
1037
1241
  #bus = null;
@@ -1102,12 +1306,6 @@ var SocketService = class extends Emittery {
1102
1306
  )
1103
1307
  );
1104
1308
  },
1105
- user: (message) => {
1106
- this.#traceBroadcast(
1107
- { target: "user", room: message.room, event: message.event, via: "bus" },
1108
- () => this.#emitToUserRoomLocally(message.room, message.event, message.data)
1109
- );
1110
- },
1111
1309
  broadcast: (message) => {
1112
1310
  this.#traceBroadcast(
1113
1311
  { target: "global", event: message.event, via: "bus" },
@@ -1229,7 +1427,6 @@ var SocketService = class extends Emittery {
1229
1427
  await this.#channelSubscriptions?.leaveAll(socket);
1230
1428
  this.#channelSubscriptions?.deleteSocket(socket.id);
1231
1429
  this.#sockets.delete(socket.id);
1232
- this.#leaveUserRoom(socket.id);
1233
1430
  this.#emitLifecycleEvent("disconnect", { socket, reason: "close" });
1234
1431
  },
1235
1432
  {
@@ -1347,40 +1544,12 @@ var SocketService = class extends Emittery {
1347
1544
  emit: (event, data) => {
1348
1545
  this.#sendSerializedEventFrame(connection, serializeFrame({ type: "event", event, data }));
1349
1546
  },
1350
- joinUserRoom: (userId) => {
1351
- this.#joinUserRoom(raw.id, userRoom(userId));
1352
- return Promise.resolve();
1353
- },
1354
- leaveUserRoom: () => {
1355
- this.#leaveUserRoom(raw.id);
1356
- return Promise.resolve();
1357
- },
1358
1547
  disconnect() {
1359
1548
  connection.close();
1360
1549
  },
1361
1550
  raw
1362
1551
  };
1363
1552
  }
1364
- #joinUserRoom(socketId, room) {
1365
- this.#leaveUserRoom(socketId);
1366
- if (!this.#userRooms.has(room)) {
1367
- this.#userRooms.set(room, /* @__PURE__ */ new Set());
1368
- }
1369
- this.#userRooms.get(room).add(socketId);
1370
- this.#socketUserRoom.set(socketId, room);
1371
- }
1372
- #leaveUserRoom(socketId) {
1373
- const room = this.#socketUserRoom.get(socketId);
1374
- if (!room) {
1375
- return;
1376
- }
1377
- const sockets = this.#userRooms.get(room);
1378
- sockets?.delete(socketId);
1379
- if (sockets?.size === 0) {
1380
- this.#userRooms.delete(room);
1381
- }
1382
- this.#socketUserRoom.delete(socketId);
1383
- }
1384
1553
  to(channel) {
1385
1554
  return {
1386
1555
  emit: (event, data) => {
@@ -1399,14 +1568,6 @@ var SocketService = class extends Emittery {
1399
1568
  })
1400
1569
  };
1401
1570
  }
1402
- toUser(userId) {
1403
- const room = userRoom(userId);
1404
- return {
1405
- emit: (event, data) => {
1406
- this.#broadcastSink.dispatch({ target: "user", userId, room, event, data });
1407
- }
1408
- };
1409
- }
1410
1571
  broadcast(event, data) {
1411
1572
  this.#broadcastSink.dispatch({ target: "global", event, data });
1412
1573
  }
@@ -1442,20 +1603,6 @@ var SocketService = class extends Emittery {
1442
1603
  );
1443
1604
  return;
1444
1605
  }
1445
- case "user": {
1446
- const room = emission.room;
1447
- const serializedFrame = serializeFrame({
1448
- type: "event",
1449
- event: emission.event,
1450
- data: emission.data
1451
- });
1452
- this.#bus?.publishUser(room, emission.event, emission.data);
1453
- this.#traceBroadcast(
1454
- { target: "user", room, event: emission.event, via: "local" },
1455
- () => this.#emitToUserRoomLocally(room, emission.event, emission.data, serializedFrame)
1456
- );
1457
- return;
1458
- }
1459
1606
  case "global": {
1460
1607
  const serializedFrame = serializeFrame({
1461
1608
  type: "event",
@@ -1495,16 +1642,6 @@ var SocketService = class extends Emittery {
1495
1642
  }
1496
1643
  return delivered;
1497
1644
  }
1498
- #emitToUserRoomLocally(room, event, data, serializedFrame = serializeFrame({ type: "event", event, data })) {
1499
- let delivered = 0;
1500
- for (const socketId of this.#userRooms.get(room) ?? []) {
1501
- const socket = this.#sockets.get(socketId);
1502
- if (socket && this.#sendSerializedEventFrame(socket.raw.connection, serializedFrame)) {
1503
- delivered += 1;
1504
- }
1505
- }
1506
- return delivered;
1507
- }
1508
1645
  #broadcastLocally(event, data, serializedFrame = serializeFrame({ type: "event", event, data })) {
1509
1646
  let delivered = 0;
1510
1647
  for (const socket of this.#sockets.values()) {
@@ -1527,23 +1664,9 @@ var SocketService = class extends Emittery {
1527
1664
  }
1528
1665
  #getLocalPresenceSockets(channel) {
1529
1666
  return (this.#presenceManager?.getLocalSockets(channel) ?? []).map((socket) => {
1530
- return this.#serializePresenceSocket(channel, socket);
1667
+ return PresenceSocketFrame.fromSocket(channel, socket).toTransport();
1531
1668
  });
1532
1669
  }
1533
- #serializePresenceSocket(channel, socket) {
1534
- const user = socket.data[PRESENCE_DATA_KEY][channel];
1535
- return {
1536
- id: socket.id,
1537
- data: {
1538
- [PRESENCE_DATA_KEY]: {
1539
- [channel]: {
1540
- ...user,
1541
- joinedAt: user.joinedAt.toISOString()
1542
- }
1543
- }
1544
- }
1545
- };
1546
- }
1547
1670
  getSocket(socketId) {
1548
1671
  return this.#sockets.get(socketId);
1549
1672
  }
@@ -1663,8 +1786,6 @@ var SocketService = class extends Emittery {
1663
1786
  #cleanup() {
1664
1787
  this.#stopHeartbeat();
1665
1788
  this.#sockets.clear();
1666
- this.#userRooms.clear();
1667
- this.#socketUserRoom.clear();
1668
1789
  this.#messageQueue.clear();
1669
1790
  this.#messageRates.clear();
1670
1791
  this.#socketFinalizations.clear();
@@ -1731,6 +1852,141 @@ var SocketService = class extends Emittery {
1731
1852
  }
1732
1853
  };
1733
1854
 
1855
+ // src/channel_pattern.ts
1856
+ var ChannelPattern = class _ChannelPattern {
1857
+ constructor(value) {
1858
+ this.value = value;
1859
+ this.syntax = ChannelPatternSyntax.from(value);
1860
+ }
1861
+ syntax;
1862
+ /**
1863
+ * Parse one runtime channel pattern.
1864
+ */
1865
+ static from(value) {
1866
+ return new _ChannelPattern(value);
1867
+ }
1868
+ /**
1869
+ * Match one concrete channel name.
1870
+ */
1871
+ match(channelName) {
1872
+ return this.syntax.match(channelName);
1873
+ }
1874
+ /**
1875
+ * Return the most specific matching candidate. Static segments have the highest score.
1876
+ * Parameters score above optional parameters. Wildcards have the lowest score.
1877
+ * Insertion order is preserved for ties.
1878
+ */
1879
+ static firstMatch(channelName, candidates) {
1880
+ const sorted = [...candidates].sort((left, right) => {
1881
+ return _ChannelPattern.#compareSpecificity(left.pattern, right.pattern);
1882
+ });
1883
+ const matched = ChannelPatternSyntax.firstMatch(
1884
+ sorted.map(({ pattern: pattern2 }) => pattern2.syntax),
1885
+ channelName
1886
+ );
1887
+ if (!matched) return null;
1888
+ const { pattern, channel } = sorted[matched.index];
1889
+ return { pattern, channel, params: matched.params };
1890
+ }
1891
+ static #compareSpecificity(left, right) {
1892
+ return left.syntax.compareSpecificity(right.syntax);
1893
+ }
1894
+ };
1895
+
1896
+ // src/channel_router.ts
1897
+ var ChannelRouter = class {
1898
+ constructor(makeChannel = (channel) => new channel()) {
1899
+ this.makeChannel = makeChannel;
1900
+ }
1901
+ /**
1902
+ * Registered channels keyed by their pattern.
1903
+ */
1904
+ #channels = /* @__PURE__ */ new Map();
1905
+ /**
1906
+ * Registers a channel.
1907
+ */
1908
+ register(channel) {
1909
+ const patternValue = channel.pattern;
1910
+ if (!patternValue) {
1911
+ throw new Error(`Channel ${channel.name} must define a static pattern`);
1912
+ }
1913
+ if (this.#channels.has(patternValue)) {
1914
+ throw new Error(`Channel pattern "${patternValue}" is already registered`);
1915
+ }
1916
+ const pattern = ChannelPattern.from(patternValue);
1917
+ this.#channels.set(pattern.value, { pattern, channel });
1918
+ }
1919
+ /**
1920
+ * Matches a channel name against registered patterns.
1921
+ */
1922
+ match(channelName) {
1923
+ const matched = ChannelPattern.firstMatch(channelName, this.#channels.values());
1924
+ if (!matched) {
1925
+ return null;
1926
+ }
1927
+ return {
1928
+ channel: matched.channel,
1929
+ pattern: matched.pattern.value,
1930
+ params: matched.params
1931
+ };
1932
+ }
1933
+ /**
1934
+ * Authorizes a socket before it joins a channel.
1935
+ */
1936
+ async authorize(socket, channelName) {
1937
+ const matched = this.match(channelName);
1938
+ if (!matched) {
1939
+ return {
1940
+ success: false,
1941
+ error: "Channel not found"
1942
+ };
1943
+ }
1944
+ const instance = await this.makeChannel(matched.channel);
1945
+ const paramValues = Object.values(matched.params);
1946
+ const ctx = {
1947
+ socket,
1948
+ channel: channelName,
1949
+ params: matched.params,
1950
+ presenceData: void 0,
1951
+ setPresenceData(data) {
1952
+ ctx.presenceData = data;
1953
+ }
1954
+ };
1955
+ try {
1956
+ await instance.$runMiddlewares(ctx);
1957
+ } catch (error) {
1958
+ return {
1959
+ success: false,
1960
+ error: error instanceof SocketResponseError ? error.message : "Authorization failed",
1961
+ cause: error
1962
+ };
1963
+ }
1964
+ return {
1965
+ success: true,
1966
+ instance,
1967
+ params: matched.params,
1968
+ paramValues,
1969
+ presenceData: ctx.presenceData ?? null
1970
+ };
1971
+ }
1972
+ /**
1973
+ * Lists all registered channels.
1974
+ */
1975
+ list() {
1976
+ return [...this.#channels.values()].map(({ pattern, channel }) => ({
1977
+ pattern: pattern.value,
1978
+ name: channel.name,
1979
+ options: channel.options
1980
+ }));
1981
+ }
1982
+ /**
1983
+ * Number of registered channels.
1984
+ */
1985
+ get size() {
1986
+ return this.#channels.size;
1987
+ }
1988
+ };
1989
+
1734
1990
  // providers/socket_provider.ts
1735
1991
  var SocketProvider = class {
1736
1992
  constructor(app) {
@@ -1741,7 +1997,7 @@ var SocketProvider = class {
1741
1997
  */
1742
1998
  register() {
1743
1999
  this.app.container.singleton("socket.router", () => {
1744
- return new ChannelRouter();
2000
+ return new ChannelRouter((channel) => this.app.container.make(channel));
1745
2001
  });
1746
2002
  this.app.container.singleton("socket.presence", () => {
1747
2003
  return new PresenceManager();
@@ -1809,7 +2065,7 @@ var SocketProvider = class {
1809
2065
  */
1810
2066
  #createUpgradeContextRunner(config, server) {
1811
2067
  const middleware = config.websocket?.middleware ?? [];
1812
- return async (request, handler) => {
2068
+ return async (request, handler, shortCircuitBeforeMiddleware) => {
1813
2069
  const response = new ServerResponse(request);
1814
2070
  const adonisRequest = server.createRequest(request, response);
1815
2071
  const adonisResponse = server.createResponse(request, response);
@@ -1818,6 +2074,10 @@ var SocketProvider = class {
1818
2074
  adonisResponse,
1819
2075
  this.app.container.createResolver()
1820
2076
  );
2077
+ const earlyResult = await shortCircuitBeforeMiddleware?.(httpContext);
2078
+ if (earlyResult !== void 0) {
2079
+ return earlyResult;
2080
+ }
1821
2081
  return this.#runUpgradeMiddleware(server, middleware, httpContext, handler);
1822
2082
  };
1823
2083
  }
@@ -1853,13 +2113,6 @@ var SocketProvider = class {
1853
2113
  */
1854
2114
  async #registerChannels(router) {
1855
2115
  const logger = await this.#resolveLogger();
1856
- const config = this.app.config.get("socket", {});
1857
- const legacyChannels = config.channels;
1858
- if (legacyChannels?.patterns) {
1859
- throw new Error(
1860
- "Socket channel patterns are managed by the Assembler hook and cannot be configured at runtime"
1861
- );
1862
- }
1863
2116
  let generated;
1864
2117
  try {
1865
2118
  generated = await this.app.import("#generated/socket_channels");