sleepy-serv 0.23.0 → 0.25.0

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.
@@ -10,7 +10,8 @@ import {
10
10
 
11
11
  import {
12
12
  StatusCode,
13
- InternalCloseSignal,
13
+ SessionType,
14
+ SessionFilter,
14
15
  toSegments,
15
16
  formatError,
16
17
  executeMiddlewareChain,
@@ -37,14 +38,9 @@ import type {
37
38
  FilterFn,
38
39
  MiddlewareChain,
39
40
  SocketCommands,
41
+ SocketData,
40
42
  SessionEntry,
41
43
  WebSocketRequest,
42
- SocketData,
43
- SocketConnection,
44
- ActiveSession,
45
- InactiveSession,
46
- Session,
47
- SocketOptions,
48
44
  } from './utils'
49
45
 
50
46
  import type {
@@ -54,7 +50,10 @@ import type {
54
50
  ResponseMessage,
55
51
  } from './messages'
56
52
 
57
- type SocketHandler = (req: Request, res: unknown) => AsyncHandlerResult
53
+ type SocketHandler<T = void> = (
54
+ req: Request<T>,
55
+ res: unknown,
56
+ ) => AsyncHandlerResult
58
57
 
59
58
  type UpgradeData = {
60
59
  clientId?: string
@@ -66,10 +65,10 @@ type UpgradeContext = {
66
65
  [key: string]: unknown
67
66
  }
68
67
 
69
- type SocketEndpoint = {
68
+ type SocketEndpoint<T = void> = {
70
69
  method: HttpMethod
71
70
  path: string
72
- handler: SocketHandler
71
+ handler: SocketHandler<T>
73
72
  }
74
73
 
75
74
  type CreateSocketRequest = {
@@ -91,30 +90,79 @@ type UpdateTicketRequest = {
91
90
  }
92
91
  }
93
92
 
94
- export type Ticket = {
93
+ export type SocketConnection<T = void> = {
94
+ data: SocketData<T>
95
+ send: (data: string) => unknown
96
+ close: (code?: number, reason?: string) => void
97
+ }
98
+
99
+ export type ActiveSession<T = void> = {
100
+ token: string
101
+ ws: SocketConnection<T>
102
+ }
103
+
104
+ export type InactiveSession<T = void> = {
105
+ token: string
106
+ expiresAt: number
107
+ data: T
108
+ }
109
+
110
+ export type ActiveSessions<T = void> = ReadonlyMap<string, ActiveSession<T>>
111
+
112
+ export const ServerCloseSignals: Record<string, CloseSignal> = {
113
+ Ok: {
114
+ code: 1000,
115
+ reason: 'ok',
116
+ },
117
+ Reaped: {
118
+ code: 4998,
119
+ reason: 'reaped',
120
+ },
121
+ Superseded: {
122
+ code: 4999,
123
+ reason: 'superseded',
124
+ },
125
+ } as const
126
+
127
+ export type ServerCloseSignals =
128
+ typeof ServerCloseSignals[keyof typeof ServerCloseSignals]
129
+
130
+ export type Session<T = void> = ActiveSession<T> | InactiveSession<T>
131
+
132
+ export type SocketOptions = {
133
+ dropThreshold?: number
134
+ heartbeatInterval?: number
135
+ maxTickets?: number
136
+ reclaimTtl?: number
137
+ ticketTtl?: number
138
+ onOpen?: (clientId: string) => void
139
+ onClose?: (clientId: string, signal: CloseSignal) => void
140
+ }
141
+
142
+ export type Ticket<T = void> = {
95
143
  clientId: string
96
144
  expiresAt: number
97
- data: unknown
145
+ data: T
98
146
  }
99
147
 
100
- export type SocketState = {
148
+ export type SocketState<T = void> = {
101
149
  dropThreshold: number
102
150
  heartbeatInterval: number
103
151
  maxTickets: number
104
152
  reclaimTtl: number
105
153
  ticketTtl: number
106
- tickets: Map<string, Ticket>
107
- activeSessions: Map<string, ActiveSession>
108
- inactiveSessions: Map<string, InactiveSession>
154
+ tickets: Map<string, Ticket<T>>
155
+ activeSessions: Map<string, ActiveSession<T>>
156
+ inactiveSessions: Map<string, InactiveSession<T>>
109
157
  onOpen: ((clientId: string) => void) | null
110
158
  onClose: ((clientId: string, signal: CloseSignal) => void) | null
111
159
  }
112
160
 
113
- export type SocketRoute = {
161
+ export type SocketRoute<T = void> = {
114
162
  method: HttpMethod
115
163
  path: string
116
164
  segments: string[]
117
- chain: MiddlewareChain
165
+ chain: MiddlewareChain<T>
118
166
  }
119
167
 
120
168
  const ajv = new Ajv({
@@ -218,7 +266,7 @@ const updateTicketValidator = ajv.compile<UpdateTicketRequest>({
218
266
  },
219
267
  })
220
268
 
221
- function isSessionActive (session: InactiveSession): boolean {
269
+ function isSessionActive<T> (session: InactiveSession<T>): boolean {
222
270
  return !session.expiresAt || session.expiresAt > Date.now()
223
271
  }
224
272
 
@@ -240,7 +288,7 @@ function parseMessage (raw: string | Buffer): RawMessage | undefined {
240
288
  }
241
289
  }
242
290
 
243
- function sweepInactiveSessions (state: SocketState): void {
291
+ function sweepInactiveSessions<T> (state: SocketState<T>): void {
244
292
  for (const [key, session] of state.inactiveSessions) {
245
293
  if (!isSessionActive(session)) {
246
294
  state.inactiveSessions.delete(key)
@@ -290,10 +338,10 @@ function matchesSegments (
290
338
  )
291
339
  }
292
340
 
293
- function matchRoute (
294
- routes: SocketRoute[],
341
+ function matchRoute<T> (
342
+ routes: SocketRoute<T>[],
295
343
  message: RequestMessage,
296
- ): SocketRoute {
344
+ ): SocketRoute<T> {
297
345
  const requestSegments = toSegments(message.route)
298
346
 
299
347
  const matchingPaths = routes.filter(route =>
@@ -313,8 +361,8 @@ function matchRoute (
313
361
  return route
314
362
  }
315
363
 
316
- function buildParams (
317
- route: SocketRoute,
364
+ function buildParams<T> (
365
+ route: SocketRoute<T>,
318
366
  message: RequestMessage,
319
367
  ): Record<string, string> {
320
368
  const requestSegments = toSegments(message.route)
@@ -327,11 +375,11 @@ function buildParams (
327
375
  } : accum, {})
328
376
  }
329
377
 
330
- function buildRequest (
378
+ function buildRequest<T> (
331
379
  params: Record<string, string>,
332
380
  message: RequestMessage,
333
- ws: SocketCommands,
334
- ): WebSocketRequest {
381
+ ws: SocketCommands<T>,
382
+ ): WebSocketRequest<T> {
335
383
  const { id, clientId, method, route } = message
336
384
  const headers = new Headers(message.headers ?? {})
337
385
  const query = message.query ?? {}
@@ -386,7 +434,9 @@ function buildErrorMessage (
386
434
  })
387
435
  }
388
436
 
389
- export function buildSocketState (opts: SocketOptions = {}): SocketState {
437
+ export function buildSocketState<T = void> (
438
+ opts: SocketOptions = {},
439
+ ): SocketState<T> {
390
440
  return {
391
441
  dropThreshold: opts.dropThreshold ?? 120_000,
392
442
  heartbeatInterval: opts.heartbeatInterval ?? 30_000,
@@ -401,11 +451,11 @@ export function buildSocketState (opts: SocketOptions = {}): SocketState {
401
451
  }
402
452
  }
403
453
 
404
- export function buildSocketServer (
405
- routes: SocketRoute[],
406
- state: SocketState,
407
- commands: SocketCommands,
408
- ): WebSocketHandler<SocketData> {
454
+ export function buildSocketServer<T = void> (
455
+ routes: SocketRoute<T>[],
456
+ state: SocketState<T>,
457
+ commands: SocketCommands<T>,
458
+ ): WebSocketHandler<SocketData<T>> {
409
459
  const {
410
460
  dropThreshold,
411
461
  heartbeatInterval,
@@ -416,7 +466,7 @@ export function buildSocketServer (
416
466
  onClose,
417
467
  } = state
418
468
 
419
- function armReaper (ws: SocketConnection) {
469
+ function armReaper (ws: SocketConnection<T>) {
420
470
  if (ws.data.reaperHandle) {
421
471
  clearTimeout(ws.data.reaperHandle)
422
472
  }
@@ -425,13 +475,13 @@ export function buildSocketServer (
425
475
  ws.data.reaped = true
426
476
 
427
477
  ws.close(
428
- InternalCloseSignal.Reaped.code,
429
- InternalCloseSignal.Reaped.reason,
478
+ ServerCloseSignals.Reaped.code,
479
+ ServerCloseSignals.Reaped.reason,
430
480
  )
431
481
  }, dropThreshold)
432
482
  }
433
483
 
434
- function invokeOpen (ws: SocketConnection) {
484
+ function invokeOpen (ws: SocketConnection<T>) {
435
485
  if (onOpen) {
436
486
  try {
437
487
  onOpen(ws.data.clientId)
@@ -441,7 +491,7 @@ export function buildSocketServer (
441
491
  }
442
492
  }
443
493
 
444
- function invokeClose (ws: SocketConnection, signal: CloseSignal) {
494
+ function invokeClose (ws: SocketConnection<T>, signal: CloseSignal) {
445
495
  if (onClose) {
446
496
  try {
447
497
  onClose(ws.data.clientId, signal)
@@ -452,7 +502,7 @@ export function buildSocketServer (
452
502
  }
453
503
 
454
504
  return {
455
- open (ws: SocketConnection): void {
505
+ open (ws: SocketConnection<T>): void {
456
506
  sweepInactiveSessions(state)
457
507
 
458
508
  const token = randomToken()
@@ -462,8 +512,8 @@ export function buildSocketServer (
462
512
  existingSession.ws.data.superseded = true
463
513
 
464
514
  existingSession.ws.close(
465
- InternalCloseSignal.Superseded.code,
466
- InternalCloseSignal.Superseded.reason,
515
+ ServerCloseSignals.Superseded.code,
516
+ ServerCloseSignals.Superseded.reason,
467
517
  )
468
518
  }
469
519
 
@@ -491,7 +541,7 @@ export function buildSocketServer (
491
541
  ws.send(JSON.stringify(welcomeMessage))
492
542
  invokeOpen(ws)
493
543
  },
494
- close (ws: SocketConnection, code: number, reason: string): void {
544
+ close (ws: SocketConnection<T>, code: number, reason: string): void {
495
545
  const signal: CloseSignal = {
496
546
  code,
497
547
  reason,
@@ -515,17 +565,20 @@ export function buildSocketServer (
515
565
 
516
566
  activeSessions.delete(ws.data.clientId)
517
567
 
518
- if (code !== InternalCloseSignal.Ok.code || ws.data.reaped) {
568
+ if (code !== ServerCloseSignals.Ok.code || ws.data.reaped) {
519
569
  inactiveSessions.set(ws.data.clientId, {
520
570
  token: exists.token,
521
571
  expiresAt: Date.now() + reclaimTtl,
522
- app: ws.data.app,
572
+ data: ws.data.data,
523
573
  })
524
574
  }
525
575
 
526
576
  invokeClose(ws, signal)
527
577
  },
528
- async message (ws: SocketConnection, raw: string | Buffer): Promise<void> {
578
+ async message (
579
+ ws: SocketConnection<T>,
580
+ raw: string | Buffer,
581
+ ): Promise<void> {
529
582
  const incomingMsg = parseMessage(raw)
530
583
 
531
584
  if (incomingMsg === undefined) {
@@ -565,7 +618,9 @@ export function buildSocketServer (
565
618
  }
566
619
  }
567
620
 
568
- export function buildSocketHandlers (state: SocketState): SocketEndpoint[] {
621
+ export function buildSocketHandlers<T = void> (
622
+ state: SocketState<T>,
623
+ ): SocketEndpoint<T>[] {
569
624
  const {
570
625
  maxTickets,
571
626
  ticketTtl,
@@ -574,7 +629,7 @@ export function buildSocketHandlers (state: SocketState): SocketEndpoint[] {
574
629
  inactiveSessions,
575
630
  } = state
576
631
 
577
- function issueTicket (clientId: string, data: unknown): string {
632
+ function issueTicket (clientId: string, data: T): string {
578
633
  for (const [key, entry] of tickets) {
579
634
  if (entry.expiresAt > Date.now()) {
580
635
  break
@@ -599,7 +654,7 @@ export function buildSocketHandlers (state: SocketState): SocketEndpoint[] {
599
654
  return hash
600
655
  }
601
656
 
602
- function redeemTicket (hash: string): Ticket | undefined {
657
+ function redeemTicket (hash: string): Ticket<T> | undefined {
603
658
  const entry = hash ? tickets.get(hash) : undefined
604
659
 
605
660
  if (!entry) {
@@ -616,7 +671,7 @@ export function buildSocketHandlers (state: SocketState): SocketEndpoint[] {
616
671
  {
617
672
  method: 'GET',
618
673
  path: '/ws',
619
- handler (req: Request, res: unknown): AsyncHandlerResult {
674
+ handler (req: Request<T>, res: unknown): AsyncHandlerResult {
620
675
  const validReq = validateSchema(req, createSocketValidator)
621
676
 
622
677
  if (typeof res !== 'object') {
@@ -635,7 +690,7 @@ export function buildSocketHandlers (state: SocketState): SocketEndpoint[] {
635
690
  ctx.data.superseded = false
636
691
  ctx.data.reaped = false
637
692
  ctx.data.reaperHandle = null
638
- ctx.data.app = ticket.data
693
+ ctx.data.data = ticket.data
639
694
 
640
695
  const useSocket = validReq.server.upgrade(validReq.raw, ctx)
641
696
 
@@ -649,11 +704,11 @@ export function buildSocketHandlers (state: SocketState): SocketEndpoint[] {
649
704
  {
650
705
  method: 'POST',
651
706
  path: '/ws',
652
- async handler (req: Request, res: unknown): AsyncHandlerResult {
707
+ async handler (req: Request<T>, res: unknown): AsyncHandlerResult {
653
708
  validateSchema(req, createTicketValidator)
654
709
 
655
710
  const clientId = crypto.randomUUID()
656
- const ticket = issueTicket(clientId, res)
711
+ const ticket = issueTicket(clientId, res as T)
657
712
 
658
713
  return Response.json({
659
714
  clientId,
@@ -665,12 +720,12 @@ export function buildSocketHandlers (state: SocketState): SocketEndpoint[] {
665
720
  {
666
721
  method: 'PUT',
667
722
  path: '/ws/:clientId',
668
- async handler (req: Request, res: unknown): AsyncHandlerResult {
723
+ async handler (req: Request<T>, res: unknown): AsyncHandlerResult {
669
724
  const validReq = validateSchema(req, updateTicketValidator)
670
725
  const authHeader = validReq.headers.get('authorization')!
671
726
  const token = authHeader.slice('Bearer '.length)
672
727
 
673
- let session: Session | undefined =
728
+ let session: Session<T> | undefined =
674
729
  activeSessions.get(validReq.params.clientId)
675
730
 
676
731
  if (!session) {
@@ -691,7 +746,7 @@ export function buildSocketHandlers (state: SocketState): SocketEndpoint[] {
691
746
  throw new UnauthorizedError('Invalid token')
692
747
  }
693
748
 
694
- const appData = 'ws' in session ? session.ws.data.app : session.app
749
+ const appData = 'ws' in session ? session.ws.data.data : session.data
695
750
 
696
751
  return Response.json({
697
752
  clientId: validReq.params.clientId,
@@ -703,7 +758,7 @@ export function buildSocketHandlers (state: SocketState): SocketEndpoint[] {
703
758
  ]
704
759
  }
705
760
 
706
- export function buildDisabledSocketCommands (): SocketCommands {
761
+ export function buildDisabledSocketCommands<T = void> (): SocketCommands<T> {
707
762
  const msg = 'WebSocket support is not enabled'
708
763
 
709
764
  return {
@@ -714,7 +769,9 @@ export function buildDisabledSocketCommands (): SocketCommands {
714
769
  }
715
770
  }
716
771
 
717
- export function buildSocketCommands (state: SocketState): SocketCommands {
772
+ export function buildSocketCommands<T = void> (
773
+ state: SocketState<T>,
774
+ ): SocketCommands<T> {
718
775
  function sendToClient (clientId: string, event: string, body: unknown) {
719
776
  const session = state.activeSessions.get(clientId)
720
777
 
@@ -737,19 +794,25 @@ export function buildSocketCommands (state: SocketState): SocketCommands {
737
794
  sendToClient(clientId, event, body)
738
795
  }
739
796
  },
740
- send (event: string, body: unknown, fn: FilterFn) {
797
+ send (event: string, body: unknown, fn: FilterFn<T>) {
741
798
  let index = 0
742
799
 
743
800
  /* TODO: look into concurrency at some point */
744
801
  for (const [clientId, session] of state.activeSessions) {
745
- if (fn(clientId, session.ws.data, index)) {
802
+ const entry: SessionEntry<T> = {
803
+ clientId,
804
+ type: SessionType.Active,
805
+ data: session.ws.data.data,
806
+ }
807
+
808
+ if (fn(entry, index)) {
746
809
  sendToClient(clientId, event, body)
747
810
  }
748
811
 
749
812
  index += 1
750
813
  }
751
814
  },
752
- drop (signal: CloseSignal, fn: FilterFn) {
815
+ drop (signal: CloseSignal, fn: FilterFn<T>) {
753
816
  const { code, reason } = signal
754
817
 
755
818
  let index = 0
@@ -761,28 +824,66 @@ export function buildSocketCommands (state: SocketState): SocketCommands {
761
824
  }
762
825
 
763
826
  for (const [clientId, session] of state.activeSessions) {
764
- if (fn(clientId, session.ws.data, index)) {
827
+ const entry: SessionEntry<T> = {
828
+ clientId,
829
+ type: SessionType.Active,
830
+ data: session.ws.data.data,
831
+ }
832
+
833
+ if (fn(entry, index)) {
765
834
  session.ws.close(code, reason)
766
835
  }
767
836
 
768
837
  index += 1
769
838
  }
770
839
  },
771
- query (fn: FilterFn) {
772
- const results: SessionEntry[] = []
773
- let index = 0
840
+ query (
841
+ fn: FilterFn<T>,
842
+ filter: SessionFilter = SessionFilter.Active,
843
+ ) {
844
+ const results: SessionEntry<T>[] = []
845
+
846
+ function processSessions (
847
+ type: SessionType,
848
+ filter: SessionFilter,
849
+ startIndex: number,
850
+ ): number {
851
+ let index = startIndex
852
+
853
+ const sessions = type === SessionType.Active
854
+ ? state.activeSessions
855
+ : state.inactiveSessions
856
+
857
+ if (type !== filter && filter !== SessionFilter.All) {
858
+ return index
859
+ }
774
860
 
775
- for (const [clientId, session] of state.activeSessions) {
776
- if (fn(clientId, session.ws.data, index)) {
777
- results.push({
861
+ for (const [clientId, session] of sessions) {
862
+ const data = 'ws' in session ? session.ws.data.data : session.data
863
+
864
+ const entry: SessionEntry<T> = {
778
865
  clientId,
779
- app: session.ws.data.app,
780
- })
866
+ type,
867
+ data,
868
+ }
869
+
870
+ if (fn(entry, index)) {
871
+ results.push(entry)
872
+ }
873
+
874
+ index += 1
781
875
  }
782
876
 
783
- index += 1
877
+ return index
784
878
  }
785
879
 
880
+ let index = 0
881
+
882
+ sweepInactiveSessions(state)
883
+
884
+ index = processSessions(SessionType.Active, filter, index)
885
+ index = processSessions(SessionType.Inactive, filter, index)
886
+
786
887
  return results
787
888
  },
788
889
  }