@rlanz/socket 0.0.1-0 → 0.0.1-10

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 (67) hide show
  1. package/README.md +594 -90
  2. package/build/base_channel-DKmR3Z0l.d.ts +476 -0
  3. package/build/chunk-55EIQ2VV.js +152 -0
  4. package/build/chunk-55EIQ2VV.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-BLRAKPXW.js +7 -0
  8. package/build/chunk-BLRAKPXW.js.map +1 -0
  9. package/build/chunk-E6ILCRL3.js +182 -0
  10. package/build/chunk-E6ILCRL3.js.map +1 -0
  11. package/build/{chunk-XUDFDJME.js → chunk-G75J233Z.js} +12 -9
  12. package/build/chunk-G75J233Z.js.map +1 -0
  13. package/build/chunk-IUQ3GYZZ.js +140 -0
  14. package/build/chunk-IUQ3GYZZ.js.map +1 -0
  15. package/build/chunk-K243S3QP.js +131 -0
  16. package/build/chunk-K243S3QP.js.map +1 -0
  17. package/build/chunk-NUPBNOP3.js +61 -0
  18. package/build/chunk-NUPBNOP3.js.map +1 -0
  19. package/build/chunk-O6UYCGTV.js +914 -0
  20. package/build/chunk-O6UYCGTV.js.map +1 -0
  21. package/build/framework-16ypv4Ss.d.ts +13 -0
  22. package/build/index-8jB5RG6v.d.ts +150 -0
  23. package/build/index.d.ts +29 -8
  24. package/build/index.js +39 -15
  25. package/build/index.js.map +1 -1
  26. package/build/providers/socket_provider.d.ts +6 -3
  27. package/build/providers/socket_provider.js +2156 -45
  28. package/build/providers/socket_provider.js.map +1 -1
  29. package/build/services/socket.d.ts +6 -3
  30. package/build/{shared_types-Dw9AphfO.d.ts → shared_types-DE86M136.d.ts} +5 -4
  31. package/build/socket_service-C1JM64_9.d.ts +106 -0
  32. package/build/src/assembler_hook.d.ts +15 -0
  33. package/build/src/assembler_hook.js +238 -0
  34. package/build/src/assembler_hook.js.map +1 -0
  35. package/build/src/client/index.d.ts +3 -106
  36. package/build/src/client/index.js +6 -667
  37. package/build/src/client/index.js.map +1 -1
  38. package/build/src/client/react.d.ts +30 -0
  39. package/build/src/client/react.js +86 -0
  40. package/build/src/client/react.js.map +1 -0
  41. package/build/src/client/types.d.ts +62 -9
  42. package/build/src/client/vue.d.ts +32 -0
  43. package/build/src/client/vue.js +100 -0
  44. package/build/src/client/vue.js.map +1 -0
  45. package/build/src/decorators.d.ts +7 -4
  46. package/build/src/decorators.js +1 -1
  47. package/build/src/health_check.d.ts +6 -3
  48. package/build/src/health_check.js +14 -3
  49. package/build/src/health_check.js.map +1 -1
  50. package/build/src/otel.d.ts +1 -0
  51. package/build/src/otel.js +187 -32
  52. package/build/src/otel.js.map +1 -1
  53. package/build/src/testing.d.ts +45 -0
  54. package/build/src/testing.js +7 -0
  55. package/build/src/testing.js.map +1 -0
  56. package/build/src/types/tracing_channels.d.ts +1 -2
  57. package/build/src/types.d.ts +4 -2
  58. package/package.json +36 -11
  59. package/build/chunk-4UPUCRVG.js +0 -1772
  60. package/build/chunk-4UPUCRVG.js.map +0 -1
  61. package/build/chunk-GKAD2UOA.js +0 -18
  62. package/build/chunk-GKAD2UOA.js.map +0 -1
  63. package/build/chunk-HK7Z65DA.js +0 -19
  64. package/build/chunk-HK7Z65DA.js.map +0 -1
  65. package/build/chunk-XUDFDJME.js.map +0 -1
  66. package/build/socket_service-jxIaH5Rs.d.ts +0 -144
  67. package/build/types-C0rDwbry.d.ts +0 -393
@@ -0,0 +1,476 @@
1
+ import { WebSocket } from 'ws';
2
+ import { IncomingMessage } from 'node:http';
3
+ import { MiddlewareAsClass } from '@adonisjs/core/types/http';
4
+ import { HttpContext } from '@adonisjs/core/http';
5
+ import { TransportFactory } from '@boringnode/bus/types/main';
6
+ import './shared_types-DE86M136.js';
7
+
8
+ declare const PRESENCE_DATA_KEY = "__rlanzSocketPresence";
9
+ type PresenceSocketData = Record<string, unknown> & {
10
+ [PRESENCE_DATA_KEY]?: Record<string, PresenceUser>;
11
+ };
12
+ interface PresenceUser {
13
+ id: string;
14
+ joinedAt: Date;
15
+ [key: string]: unknown;
16
+ }
17
+ interface PresenceData {
18
+ channel: string;
19
+ users: PresenceUser[];
20
+ count: number;
21
+ }
22
+ interface PresenceSocket {
23
+ id: string;
24
+ data: PresenceSocketData;
25
+ }
26
+ /**
27
+ * Tracks user presence in channels.
28
+ *
29
+ * channel -> socketId -> PresenceUser
30
+ */
31
+ declare class PresenceManager {
32
+ #private;
33
+ /**
34
+ * Sets the socket fetcher used to build distributed snapshots.
35
+ */
36
+ setSocketFetcher(fetchSockets: ((channel: string) => Promise<PresenceSocket[]>) | null): void;
37
+ /**
38
+ * Adds a user to a channel.
39
+ */
40
+ join(channel: string, socket: PresenceSocket, member: PresenceInfo): PresenceUser;
41
+ /**
42
+ * Removes a user from a channel.
43
+ */
44
+ leave(channel: string, socket: PresenceSocket): void;
45
+ /**
46
+ * Removes a user from every channel.
47
+ * Returns the list of channels the user left.
48
+ */
49
+ leaveAll(socket: PresenceSocket): string[];
50
+ /**
51
+ * Gets presence data for a channel.
52
+ */
53
+ snapshot(channel: string): Promise<PresenceData>;
54
+ /**
55
+ * Gets the number of users in a channel.
56
+ */
57
+ count(channel: string): Promise<number>;
58
+ /**
59
+ * Checks whether a socket is locally present in a channel.
60
+ */
61
+ hasLocal(channel: string, socketId: string): boolean;
62
+ /**
63
+ * Gets the presence user retained for one local socket.
64
+ */
65
+ getLocalUser(channel: string, socketId: string): PresenceUser | undefined;
66
+ /**
67
+ * Lists every local channel where a socket is present.
68
+ */
69
+ getLocalChannelsForSocket(socketId: string): string[];
70
+ /**
71
+ * Lists sockets that are locally present in a channel.
72
+ */
73
+ getLocalSockets(channel: string): PresenceSocket[];
74
+ }
75
+
76
+ /**
77
+ * Duration can be specified as milliseconds or as a human-readable string.
78
+ */
79
+ type Duration = number | string;
80
+
81
+ interface SocketTransportConfig {
82
+ driver: TransportFactory;
83
+ channel?: string;
84
+ /**
85
+ * How long to wait for other instances to answer a presence snapshot request.
86
+ */
87
+ presenceTimeout?: Duration;
88
+ /**
89
+ * In-memory retry policy for transport publications that fail.
90
+ *
91
+ * The queue is enabled and limited to 1000 distinct messages by default.
92
+ * Set maxSize to null only when an unbounded queue is explicitly desired.
93
+ */
94
+ retryQueue?: SocketRetryQueueConfig;
95
+ }
96
+ interface SocketRetryQueueConfig {
97
+ enabled?: boolean;
98
+ removeDuplicates?: boolean;
99
+ maxSize?: number | null;
100
+ retryInterval?: Duration | false;
101
+ }
102
+
103
+ interface RawSocket {
104
+ id: string;
105
+ data: Record<string, unknown>;
106
+ connection: WebSocket;
107
+ request: IncomingMessage;
108
+ httpContext: HttpContext;
109
+ }
110
+ /**
111
+ * Socket exposed to channels after the HTTP upgrade.
112
+ */
113
+ interface AuthenticatedSocket<User = unknown> {
114
+ /**
115
+ * Unique socket ID.
116
+ */
117
+ id: string;
118
+ /**
119
+ * Authenticated user resolved during the HTTP upgrade.
120
+ */
121
+ user: User | undefined;
122
+ /**
123
+ * Returns the authenticated user or throws a client-safe unauthorized error.
124
+ */
125
+ getUserOrFail(): User;
126
+ /**
127
+ * Emits an event to this socket.
128
+ */
129
+ emit(event: string, data: unknown): void;
130
+ /**
131
+ * Disconnects the socket.
132
+ */
133
+ disconnect(): void;
134
+ /**
135
+ * Raw WebSocket connection metadata.
136
+ */
137
+ raw: RawSocket;
138
+ }
139
+ /**
140
+ * Short-lived context for preparing and authenticating a WebSocket upgrade.
141
+ * Socket-lifetime mutable state belongs in AuthenticatedSocket.raw.data.
142
+ */
143
+ interface SocketUpgradeContext {
144
+ httpContext: HttpContext;
145
+ }
146
+ type SocketAuthenticationResult<User = unknown> = User | false | null;
147
+ type SocketAuthenticationHandler<User = unknown> = (ctx: SocketUpgradeContext) => SocketAuthenticationResult<User> | Promise<SocketAuthenticationResult<User>>;
148
+ type SocketOriginValues = boolean | string | string[];
149
+ type SocketOriginResolver = (origin: string, httpContext: HttpContext) => SocketOriginValues;
150
+ /**
151
+ * AdonisJS HTTP middleware executed for the WebSocket upgrade request.
152
+ */
153
+ type SocketHttpMiddleware = (() => Promise<{
154
+ default: MiddlewareAsClass;
155
+ }>) | MiddlewareAsClass;
156
+ /**
157
+ * Result of matching a channel.
158
+ */
159
+ interface ChannelMatch {
160
+ channel: BaseChannelConstructor;
161
+ pattern: string;
162
+ params: Record<string, string>;
163
+ }
164
+ /**
165
+ * Channel options.
166
+ */
167
+ interface ChannelOptions {
168
+ presence?: boolean;
169
+ }
170
+ /**
171
+ * Identity and channel-specific data returned for a presence member.
172
+ */
173
+ interface PresenceInfo {
174
+ id: string;
175
+ data?: Record<string, unknown>;
176
+ }
177
+ /**
178
+ * Presence member exposed to snapshots and channel hooks.
179
+ */
180
+ interface PresenceMember {
181
+ id: string;
182
+ joinedAt?: never;
183
+ [key: string]: unknown;
184
+ }
185
+ /**
186
+ * Context passed to middlewares.
187
+ */
188
+ interface MiddlewareContext<User = unknown> {
189
+ socket: AuthenticatedSocket<User>;
190
+ channel: string;
191
+ params: Record<string, string>;
192
+ presenceData?: Record<string, unknown> | PresenceData;
193
+ setPresenceData(data: Record<string, unknown> | PresenceData): void;
194
+ }
195
+ /**
196
+ * Middleware function.
197
+ */
198
+ type MiddlewareFn<User = unknown> = (ctx: MiddlewareContext<User>, next: () => Promise<void>) => Promise<void>;
199
+ /**
200
+ * Middleware class.
201
+ */
202
+ interface MiddlewareClass<User = unknown> {
203
+ handle: MiddlewareFn<User>;
204
+ }
205
+ /**
206
+ * Constructor for a middleware resolved through the AdonisJS container.
207
+ */
208
+ interface MiddlewareConstructor<User = unknown> {
209
+ new (...args: any[]): MiddlewareClass<User>;
210
+ readonly prototype: MiddlewareClass<User>;
211
+ }
212
+ /**
213
+ * Middleware, either a function, an instance, or a container-resolved class.
214
+ */
215
+ type Middleware<User = unknown> = MiddlewareFn<User> | MiddlewareClass<User> | MiddlewareConstructor<User>;
216
+ /**
217
+ * Package configuration.
218
+ */
219
+ interface SocketConfig<User = unknown> {
220
+ websocket?: {
221
+ path?: string;
222
+ /**
223
+ * Browser origin policy for opening a WebSocket connection.
224
+ *
225
+ * Uses the same value shapes as the AdonisJS CORS `origin` option. A resolver
226
+ * receives the request origin and HTTP context and may return any static value.
227
+ * Same-origin requests are allowed by default when this option is not defined.
228
+ * Clients without an Origin header are always allowed.
229
+ */
230
+ origin?: SocketOriginValues | SocketOriginResolver;
231
+ /**
232
+ * HTTP middleware executed only for the WebSocket upgrade request.
233
+ *
234
+ * Use this to initialize request-scoped services such as session or auth before
235
+ * websocket.authenticate. Channel middleware should be declared on channel classes.
236
+ */
237
+ middleware?: SocketHttpMiddleware[];
238
+ authenticate?: SocketAuthenticationHandler<User>;
239
+ /**
240
+ * Sends a WebSocket ping every configured duration.
241
+ *
242
+ * When only pingTimeout is configured, the service uses a default 25s interval.
243
+ */
244
+ pingInterval?: Duration;
245
+ /**
246
+ * Terminates connections that do not answer a ping within the configured duration.
247
+ *
248
+ * When only pingInterval is configured, the service uses a default 5s timeout.
249
+ */
250
+ pingTimeout?: Duration;
251
+ /**
252
+ * Maximum inbound WebSocket message payload size in bytes.
253
+ */
254
+ maxPayload?: number;
255
+ /**
256
+ * Maximum unresolved non-ping protocol messages per socket.
257
+ */
258
+ maxQueuedMessages?: number;
259
+ /**
260
+ * Maximum inbound protocol messages accepted per rate limit interval.
261
+ */
262
+ maxMessagesPerInterval?: number;
263
+ /**
264
+ * Duration of the inbound protocol message rate limit window.
265
+ */
266
+ messageRateInterval?: Duration;
267
+ /**
268
+ * Maximum buffered outbound WebSocket bytes accepted before the service closes the socket.
269
+ */
270
+ maxBufferedAmount?: number;
271
+ /**
272
+ * Maximum serialized size of one outbound WebSocket protocol message in bytes.
273
+ */
274
+ maxOutboundPayload?: number;
275
+ /**
276
+ * Maximum number of active channel subscriptions retained by one socket.
277
+ */
278
+ maxSubscriptionsPerSocket?: number;
279
+ /**
280
+ * Maximum accepted channel name length in characters.
281
+ */
282
+ maxChannelNameLength?: number;
283
+ /**
284
+ * Maximum time spent waiting for socket handlers and subscription hooks during shutdown.
285
+ * Internal socket state is forcibly released after this deadline.
286
+ */
287
+ shutdownTimeout?: Duration;
288
+ };
289
+ /**
290
+ * Optional transport used to synchronize broadcasts between SocketService instances.
291
+ */
292
+ transport?: SocketTransportConfig | null;
293
+ }
294
+ type SocketServiceStatus = 'starting' | 'ready' | 'stopping' | 'stopped' | 'failed';
295
+ interface SocketHealthSnapshot {
296
+ status: SocketServiceStatus;
297
+ ready: boolean;
298
+ connections: number;
299
+ channels: number;
300
+ lastError?: string;
301
+ }
302
+ /**
303
+ * Events emitted by the service.
304
+ */
305
+ interface SocketEvents<User = unknown> {
306
+ connect: {
307
+ socket: AuthenticatedSocket<User>;
308
+ };
309
+ disconnect: {
310
+ socket: AuthenticatedSocket<User>;
311
+ reason: string;
312
+ };
313
+ subscribe: {
314
+ socket: AuthenticatedSocket<User>;
315
+ channel: string;
316
+ };
317
+ unsubscribe: {
318
+ socket: AuthenticatedSocket<User>;
319
+ channel: string;
320
+ };
321
+ }
322
+
323
+ interface ChannelBroadcaster {
324
+ to(channel: string): {
325
+ emit(event: string, data: unknown): void;
326
+ except(socketId: string): {
327
+ emit(event: string, data: unknown): void;
328
+ };
329
+ };
330
+ }
331
+ type ChannelMiddlewareRunner = <User>(middleware: MiddlewareConstructor<User>, ctx: MiddlewareContext<User>, next: () => Promise<void>) => Promise<void>;
332
+ type ChannelMiddlewareRunnerFactory = () => ChannelMiddlewareRunner;
333
+ /**
334
+ * Error whose message may intentionally be returned to a socket client.
335
+ */
336
+ declare class SocketResponseError extends Error {
337
+ }
338
+ /**
339
+ * Abstract class used to define a channel.
340
+ *
341
+ * @example
342
+ * ```ts
343
+ * export default class ChatChannel extends BaseChannel {
344
+ * static pattern = 'chat/:roomId'
345
+ * static middlewares = [
346
+ * async (ctx, next) => {
347
+ * // Authorize access to the channel through middleware.
348
+ * // if (!Room.isMember(ctx.params.roomId, ctx.socket.user.id)) throw new Error('Forbidden')
349
+ * await next()
350
+ * },
351
+ * ]
352
+ * }
353
+ * ```
354
+ *
355
+ * @example With presence and decorated handlers
356
+ * ```ts
357
+ * import { onMessage } from '@rlanz/socket/decorators'
358
+ *
359
+ * export default class ChatChannel extends BaseChannel {
360
+ * static pattern = 'chat/:roomId'
361
+ * static options = { presence: true }
362
+ *
363
+ * static middlewares = [
364
+ * async (ctx, next) => {
365
+ * if (!ctx.socket.user.isVerified) throw new Error('Not verified')
366
+ * await next()
367
+ * }
368
+ * ]
369
+ *
370
+ * @onMessage('chat:send')
371
+ * async handleSend(socket: AuthenticatedSocket<User>, data: unknown) {
372
+ * // Handle chat:send event
373
+ * }
374
+ *
375
+ * @onMessage('chat:typing')
376
+ * async handleTyping(socket: AuthenticatedSocket<User>, data: unknown) {
377
+ * // Handle chat:typing event
378
+ * }
379
+ * }
380
+ * ```
381
+ */
382
+ declare class BaseChannel<User = unknown, ServerEvents = Record<never, never>> {
383
+ /** Type-only server event declarations consumed by generated browser client types. */
384
+ readonly $serverEvents: ServerEvents;
385
+ /**
386
+ * URL-like channel pattern, for example: 'chat/:roomId'.
387
+ */
388
+ static pattern: string;
389
+ /**
390
+ * Channel options.
391
+ */
392
+ static options?: ChannelOptions;
393
+ /**
394
+ * Middlewares to run before subscription.
395
+ */
396
+ static middlewares?: Middleware<any>[];
397
+ /**
398
+ * Injected socket service.
399
+ */
400
+ protected socket: ChannelBroadcaster;
401
+ /**
402
+ * Full channel name, for example: 'chat/general'.
403
+ */
404
+ protected channelName: string;
405
+ /**
406
+ * Parameters extracted from the pattern, for example: { roomId: 'general' }.
407
+ */
408
+ protected params: Record<string, string>;
409
+ /**
410
+ * Injected presence manager when presence is enabled.
411
+ */
412
+ protected presence: PresenceManager;
413
+ /**
414
+ * Injects dependencies.
415
+ */
416
+ $setContext(socket: ChannelBroadcaster, channelName: string, params: Record<string, string>, presence?: PresenceManager): void;
417
+ /**
418
+ * Emits to every channel member.
419
+ */
420
+ protected broadcast<K extends keyof ServerEvents & string>(event: K, data: ServerEvents[K]): void;
421
+ /**
422
+ * Emits to every channel member except one socket.
423
+ */
424
+ protected broadcastExcept<K extends keyof ServerEvents & string>(socketId: string, event: K, data: ServerEvents[K]): void;
425
+ /**
426
+ * Runs the middleware chain.
427
+ *
428
+ * middleware[0] -> middleware[1] -> ... -> subscribe
429
+ */
430
+ $runMiddlewares(ctx: MiddlewareContext<User>, runClassMiddleware?: ChannelMiddlewareRunner): Promise<void>;
431
+ /**
432
+ * Returns member information for presence.
433
+ * Implement when options.presence = true.
434
+ */
435
+ getPresenceInfo?(socket: AuthenticatedSocket<User>): PresenceInfo;
436
+ /**
437
+ * Hook called when a socket joins the channel.
438
+ */
439
+ onJoin?(socket: AuthenticatedSocket<User>, ...params: string[]): Promise<void>;
440
+ /**
441
+ * Hook called when a socket leaves the channel.
442
+ */
443
+ onLeave?(socket: AuthenticatedSocket<User>, ...params: string[]): Promise<void>;
444
+ /**
445
+ * Hook called when a member joins a presence channel.
446
+ */
447
+ onMemberJoin?(socket: AuthenticatedSocket<User>, member: PresenceMember, ...params: string[]): Promise<void>;
448
+ /**
449
+ * Hook called when a member leaves a presence channel.
450
+ */
451
+ onMemberLeave?(socket: AuthenticatedSocket<User>, member: PresenceMember, ...params: string[]): Promise<void>;
452
+ /**
453
+ * Dispatches a message to the matching handler.
454
+ * Called by SocketService. Do not override.
455
+ */
456
+ $handleMessage(socket: AuthenticatedSocket<User>, event: string, data: unknown): Promise<unknown>;
457
+ /**
458
+ * Runs the generic join hook. ChannelSubscriptions owns subscription lifecycle.
459
+ */
460
+ $handleJoin(socket: AuthenticatedSocket<User>, ...params: string[]): Promise<void>;
461
+ /**
462
+ * Runs the generic leave hook. ChannelSubscriptions owns subscription lifecycle.
463
+ */
464
+ $handleLeave(socket: AuthenticatedSocket<User>, ...params: string[]): Promise<void>;
465
+ $getPresenceInfo(socket: AuthenticatedSocket<User>): PresenceInfo;
466
+ }
467
+ /** Constructor for any BaseChannel specialization, including container-injected dependencies. */
468
+ interface BaseChannelConstructor {
469
+ new (...args: any[]): BaseChannel<any, any>;
470
+ readonly name: string;
471
+ pattern: string;
472
+ options?: ChannelOptions;
473
+ middlewares?: Middleware<any>[];
474
+ }
475
+
476
+ export { type AuthenticatedSocket as A, BaseChannel as B, type ChannelOptions as C, type Duration as D, type Middleware as M, PresenceManager as P, type RawSocket as R, type SocketAuthenticationHandler as S, type SocketConfig as a, type MiddlewareClass as b, type MiddlewareConstructor as c, type MiddlewareContext as d, type MiddlewareFn as e, type PresenceInfo as f, type PresenceMember as g, type SocketAuthenticationResult as h, type SocketEvents as i, type SocketHealthSnapshot as j, type SocketHttpMiddleware as k, type SocketOriginResolver as l, type SocketOriginValues as m, SocketResponseError as n, type SocketRetryQueueConfig as o, type SocketServiceStatus as p, type SocketTransportConfig as q, type SocketUpgradeContext as r, type BaseChannelConstructor as s, type ChannelMiddlewareRunnerFactory as t, type ChannelMatch as u, type PresenceData as v };
@@ -0,0 +1,152 @@
1
+ import {
2
+ buildChannelName,
3
+ callClientHandler,
4
+ resolveChannel
5
+ } from "./chunk-O6UYCGTV.js";
6
+
7
+ // src/client/framework.ts
8
+ function createChannelCallArgs(pattern, parameters) {
9
+ return parameters === void 0 ? [pattern] : [pattern, parameters];
10
+ }
11
+ function resolveChannelEventArgs(args) {
12
+ return {
13
+ parameterInput: args.length === 3 ? args[0] : void 0,
14
+ event: args.at(-2),
15
+ handler: args.at(-1)
16
+ };
17
+ }
18
+ var socketEntries = /* @__PURE__ */ new WeakMap();
19
+ var socketOwners = /* @__PURE__ */ new WeakMap();
20
+ function reportSubscriptionError(error) {
21
+ queueMicrotask(() => {
22
+ console.error(error);
23
+ });
24
+ }
25
+ function attachEvents(entry) {
26
+ for (const [event, managed] of entry.events) {
27
+ entry.channel.$listen(event, managed.dispatch);
28
+ }
29
+ }
30
+ function getEntry(socket, name) {
31
+ let entries = socketEntries.get(socket);
32
+ if (!entries) {
33
+ entries = /* @__PURE__ */ new Map();
34
+ socketEntries.set(socket, entries);
35
+ }
36
+ const channel = socket[resolveChannel](name);
37
+ let entry = entries.get(name);
38
+ if (!entry || entry.channel !== channel) {
39
+ entry = {
40
+ channel,
41
+ socket,
42
+ leases: /* @__PURE__ */ new Set(),
43
+ events: /* @__PURE__ */ new Map(),
44
+ lifecycle: void 0
45
+ };
46
+ entries.set(name, entry);
47
+ }
48
+ return entry;
49
+ }
50
+ var ChannelLease = class {
51
+ entry;
52
+ #released = false;
53
+ #listeners = [];
54
+ constructor(entry) {
55
+ this.entry = entry;
56
+ }
57
+ listen(event, handler) {
58
+ if (this.#released) return;
59
+ let managed = this.entry.events.get(event);
60
+ if (!managed) {
61
+ const handlers = /* @__PURE__ */ new Set();
62
+ managed = {
63
+ handlers,
64
+ dispatch: (data) => {
65
+ for (const listener of handlers) callClientHandler(listener, data);
66
+ }
67
+ };
68
+ this.entry.events.set(event, managed);
69
+ this.entry.channel.$listen(event, managed.dispatch);
70
+ }
71
+ const registration = (data) => handler(data);
72
+ managed.handlers.add(registration);
73
+ this.#listeners.push({ event, handler: registration });
74
+ }
75
+ release() {
76
+ if (this.#released) return;
77
+ this.#released = true;
78
+ for (const { event, handler } of this.#listeners) {
79
+ const managed = this.entry.events.get(event);
80
+ managed?.handlers.delete(handler);
81
+ if (managed?.handlers.size === 0) {
82
+ this.entry.channel.$stopListening(event, managed.dispatch);
83
+ this.entry.events.delete(event);
84
+ }
85
+ }
86
+ this.#listeners = [];
87
+ this.entry.leases.delete(this);
88
+ queueMicrotask(() => {
89
+ if (this.entry.leases.size > 0) return;
90
+ const lifecycle = this.entry.lifecycle;
91
+ this.entry.lifecycle = void 0;
92
+ void lifecycle?.release().then(() => {
93
+ if (this.entry.leases.size > 0 || this.entry.lifecycle) return;
94
+ const entries = socketEntries.get(this.entry.socket);
95
+ if (entries?.get(this.entry.channel.name) === this.entry) {
96
+ entries.delete(this.entry.channel.name);
97
+ }
98
+ });
99
+ });
100
+ }
101
+ };
102
+ function acquireChannel(socket, ...args) {
103
+ const [pattern, parameters] = args;
104
+ const name = buildChannelName(pattern, parameters);
105
+ const entry = getEntry(socket, name);
106
+ const lease = new ChannelLease(entry);
107
+ entry.leases.add(lease);
108
+ if (!entry.lifecycle) {
109
+ const lifecycle = entry.channel.$acquire();
110
+ entry.lifecycle = lifecycle;
111
+ attachEvents(entry);
112
+ void lifecycle.ready.then(() => {
113
+ if (entry.lifecycle === lifecycle) attachEvents(entry);
114
+ }).catch((error) => {
115
+ if (entry.lifecycle === lifecycle && entry.leases.size > 0 && entry.socket.connected) {
116
+ reportSubscriptionError(error);
117
+ }
118
+ });
119
+ }
120
+ return { channel: entry.channel, lease };
121
+ }
122
+ function acquireSocketOwnership(socket) {
123
+ let owner = socketOwners.get(socket);
124
+ if (!owner) {
125
+ owner = { count: 0, generation: 0 };
126
+ socketOwners.set(socket, owner);
127
+ socket.connect().catch(reportSubscriptionError);
128
+ }
129
+ owner.count++;
130
+ owner.generation++;
131
+ let released = false;
132
+ return () => {
133
+ if (released) return;
134
+ released = true;
135
+ owner.count--;
136
+ const generation = ++owner.generation;
137
+ queueMicrotask(() => {
138
+ if (owner.count === 0 && owner.generation === generation) {
139
+ socket.disconnect();
140
+ socketOwners.delete(socket);
141
+ }
142
+ });
143
+ };
144
+ }
145
+
146
+ export {
147
+ createChannelCallArgs,
148
+ resolveChannelEventArgs,
149
+ acquireChannel,
150
+ acquireSocketOwnership
151
+ };
152
+ //# sourceMappingURL=chunk-55EIQ2VV.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/client/framework.ts"],"sourcesContent":["import type { Channel } from './channel.js'\nimport { buildChannelName, resolveChannel, type Socket } from './socket.js'\nimport type {\n ChannelCallArgs,\n ChannelContract,\n ChannelContractFor,\n ChannelParameters,\n ChannelPattern,\n EventHandler,\n} from './types.js'\nimport { callClientHandler } from './callback.js'\n\ntype AnySocket = Socket<any>\ntype ServerEvents<Contract> =\n Contract extends ChannelContract<unknown, infer Events> ? Events : unknown\n\ntype FrameworkChannelMember<Registry, Pattern extends string> =\n ChannelContractFor<Registry, Pattern> extends infer Contract\n ? [Contract] extends [never]\n ? never\n : Channel<Contract>\n : never\n\nexport type FrameworkChannel<Registry, Pattern extends string> = FrameworkChannelMember<\n Registry,\n Pattern\n>\n\nexport type FrameworkChannelPattern<Registry> = ChannelPattern<Registry>\nexport type FrameworkChannelCallArgs<Registry> = ChannelCallArgs<Registry>\nexport type FrameworkChannelParameters<\n Registry,\n Pattern extends ChannelPattern<Registry>,\n> = ChannelParameters<Registry, Pattern>\n\nexport function createChannelCallArgs<Registry>(\n pattern: ChannelPattern<Registry>,\n parameters?: object\n): ChannelCallArgs<Registry> {\n return (parameters === undefined ? [pattern] : [pattern, parameters]) as ChannelCallArgs<Registry>\n}\n\n/** Resolves the shared positional arguments accepted by framework channel-event helpers. @internal */\nexport function resolveChannelEventArgs<ParameterInput, Event extends string, Payload>(\n args: readonly unknown[]\n): {\n parameterInput: ParameterInput | undefined\n event: Event\n handler: EventHandler<Payload>\n} {\n return {\n parameterInput: (args.length === 3 ? args[0] : undefined) as ParameterInput | undefined,\n event: args.at(-2) as Event,\n handler: args.at(-1) as EventHandler<Payload>,\n }\n}\n\nexport type FrameworkChannelEvent<Registry, Pattern extends string> =\n FrameworkChannel<Registry, Pattern> extends Channel<infer Contract>\n ? Extract<keyof ServerEvents<Contract>, string>\n : never\n\nexport type FrameworkChannelEventPayload<\n Registry,\n Pattern extends string,\n Event extends FrameworkChannelEvent<Registry, Pattern>,\n> =\n FrameworkChannel<Registry, Pattern> extends Channel<infer Contract>\n ? Event extends keyof ServerEvents<Contract>\n ? ServerEvents<Contract>[Event]\n : never\n : never\n\ninterface ManagedEvent {\n handlers: Set<EventHandler<any>>\n dispatch: EventHandler<any>\n}\n\ninterface ManagedEntry {\n channel: Channel<any>\n socket: AnySocket\n leases: Set<ChannelLease>\n events: Map<string, ManagedEvent>\n lifecycle: { ready: Promise<Channel<any>>; release: () => Promise<void> } | undefined\n}\n\nconst socketEntries = new WeakMap<AnySocket, Map<string, ManagedEntry>>()\nconst socketOwners = new WeakMap<AnySocket, { count: number; generation: number }>()\n\nfunction reportSubscriptionError(error: unknown): void {\n queueMicrotask(() => {\n console.error(error)\n })\n}\n\nfunction attachEvents(entry: ManagedEntry): void {\n for (const [event, managed] of entry.events) {\n entry.channel.$listen(event, managed.dispatch)\n }\n}\n\nfunction getEntry(socket: AnySocket, name: string): ManagedEntry {\n let entries = socketEntries.get(socket)\n if (!entries) {\n entries = new Map()\n socketEntries.set(socket, entries)\n }\n\n const channel = socket[resolveChannel](name)\n let entry = entries.get(name)\n if (!entry || entry.channel !== channel) {\n entry = {\n channel,\n socket,\n leases: new Set(),\n events: new Map(),\n lifecycle: undefined,\n }\n entries.set(name, entry)\n }\n\n return entry\n}\n\nexport class ChannelLease {\n readonly entry: ManagedEntry\n #released = false\n #listeners: Array<{ event: string; handler: EventHandler<any> }> = []\n\n constructor(entry: ManagedEntry) {\n this.entry = entry\n }\n\n listen(event: string, handler: EventHandler<any>): void {\n if (this.#released) return\n\n let managed = this.entry.events.get(event)\n if (!managed) {\n const handlers = new Set<EventHandler<any>>()\n managed = {\n handlers,\n dispatch: (data) => {\n for (const listener of handlers) callClientHandler(listener, data)\n },\n }\n this.entry.events.set(event, managed)\n this.entry.channel.$listen(event, managed.dispatch)\n }\n const registration: EventHandler<any> = (data) => handler(data)\n managed.handlers.add(registration)\n this.#listeners.push({ event, handler: registration })\n }\n\n release(): void {\n if (this.#released) return\n this.#released = true\n\n for (const { event, handler } of this.#listeners) {\n const managed = this.entry.events.get(event)\n managed?.handlers.delete(handler)\n if (managed?.handlers.size === 0) {\n this.entry.channel.$stopListening(event, managed.dispatch)\n this.entry.events.delete(event)\n }\n }\n this.#listeners = []\n this.entry.leases.delete(this)\n\n queueMicrotask(() => {\n if (this.entry.leases.size > 0) return\n const lifecycle = this.entry.lifecycle\n this.entry.lifecycle = undefined\n void lifecycle?.release().then(() => {\n if (this.entry.leases.size > 0 || this.entry.lifecycle) return\n const entries = socketEntries.get(this.entry.socket)\n if (entries?.get(this.entry.channel.name) === this.entry) {\n entries.delete(this.entry.channel.name)\n }\n })\n })\n }\n}\n\nexport function acquireChannel<Registry, const Args extends ChannelCallArgs<Registry>>(\n socket: Socket<Registry>,\n ...args: Args\n): { channel: FrameworkChannel<Registry, Args[0]>; lease: ChannelLease } {\n const [pattern, parameters] = args as [string, object?]\n const name = buildChannelName(pattern, parameters)\n const entry = getEntry(socket, name)\n const lease = new ChannelLease(entry)\n entry.leases.add(lease)\n\n if (!entry.lifecycle) {\n const lifecycle = entry.channel.$acquire()\n entry.lifecycle = lifecycle\n attachEvents(entry)\n void lifecycle.ready\n .then(() => {\n if (entry.lifecycle === lifecycle) attachEvents(entry)\n })\n .catch((error) => {\n if (entry.lifecycle === lifecycle && entry.leases.size > 0 && entry.socket.connected) {\n reportSubscriptionError(error)\n }\n })\n }\n\n return { channel: entry.channel as FrameworkChannel<Registry, Args[0]>, lease }\n}\n\n/** Connects an exclusively owned socket and coalesces development-mode remounts. */\nexport function acquireSocketOwnership(socket: AnySocket): () => void {\n let owner = socketOwners.get(socket)\n if (!owner) {\n owner = { count: 0, generation: 0 }\n socketOwners.set(socket, owner)\n socket.connect().catch(reportSubscriptionError)\n }\n owner.count++\n owner.generation++\n\n let released = false\n return () => {\n if (released) return\n released = true\n owner.count--\n const generation = ++owner.generation\n queueMicrotask(() => {\n if (owner.count === 0 && owner.generation === generation) {\n socket.disconnect()\n socketOwners.delete(socket)\n }\n })\n }\n}\n"],"mappings":"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","names":[]}