@stacksjs/cloud 0.58.53 → 0.58.55

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.
@@ -1,830 +0,0 @@
1
- /* eslint-disable eslint-comments/no-unlimited-disable */
2
- /* eslint-disable */
3
- import process from 'node:process'
4
- import { AwsClient } from 'aws4fetch'
5
- import type { Server, ServerWebSocket } from 'bun'
6
-
7
- interface Lambda {
8
- fetch: (request: Request, server: Server) => Promise<Response | undefined>
9
- error?: (error: unknown) => Promise<Response>
10
- websocket?: {
11
- open?: (ws: ServerWebSocket) => Promise<void>
12
- message?: (ws: ServerWebSocket, message: string) => Promise<void>
13
- close?: (ws: ServerWebSocket, code: number, reason: string) => Promise<void>
14
- }
15
- }
16
-
17
- let requestId: string | undefined
18
- let traceId: string | undefined
19
- let functionArn: string | undefined
20
- let aws: AwsClient | undefined
21
-
22
- const logger = console.log
23
-
24
- function log(level: string, ...args: any[]): void {
25
- if (!args.length)
26
- return
27
-
28
- const messages = args.map(arg => Bun.inspect(arg).replace(/\n/g, '\r'))
29
- if (requestId === undefined)
30
- logger(level, ...messages)
31
-
32
- else
33
- logger(level, `RequestId: ${requestId}`, ...messages)
34
- }
35
-
36
- console.log = (...args: any[]) => log('INFO', ...args)
37
- console.info = (...args: any[]) => log('INFO', ...args)
38
- console.warn = (...args: any[]) => log('WARN', ...args)
39
- console.error = (...args: any[]) => log('ERROR', ...args)
40
- console.debug = (...args: any[]) => log('DEBUG', ...args)
41
- console.trace = (...args: any[]) => log('TRACE', ...args)
42
-
43
- let warnings: Set<string> | undefined
44
-
45
- function warnOnce(message: string, ...args: any[]): void {
46
- if (warnings === undefined)
47
- warnings = new Set()
48
-
49
- if (warnings.has(message))
50
- return
51
-
52
- warnings.add(message)
53
- console.warn(message, ...args)
54
- }
55
-
56
- function reset(): void {
57
- requestId = undefined
58
- traceId = undefined
59
- warnings = undefined
60
- }
61
-
62
- function exit(...cause: any[]): never {
63
- console.error(...cause)
64
- process.exit(1)
65
- }
66
-
67
- function env(name: string, fallback?: string): string {
68
- const value = process.env[name] ?? fallback ?? null
69
- if (value === null)
70
- exit(`Runtime failed to find the '${name}' environment variable`)
71
-
72
- return value
73
- }
74
-
75
- const runtimeUrl = new URL(`http://${env('AWS_LAMBDA_RUNTIME_API')}/2018-06-01/`)
76
-
77
- async function fetch(url: string, options?: RequestInit): Promise<Response> {
78
- const { href } = new URL(url, runtimeUrl)
79
- const response = await globalThis.fetch(href, {
80
- ...options,
81
- timeout: false,
82
- })
83
- if (!response.ok)
84
- exit(`Runtime failed to send request to Lambda [status: ${response.status}]`)
85
-
86
- return response
87
- }
88
-
89
- async function fetchAws(url: string, options?: RequestInit): Promise<Response> {
90
- if (aws === undefined) {
91
- aws = new AwsClient({
92
- accessKeyId: env('AWS_ACCESS_KEY_ID'),
93
- secretAccessKey: env('AWS_SECRET_ACCESS_KEY'),
94
- sessionToken: env('AWS_SESSION_TOKEN'),
95
- region: env('AWS_REGION'),
96
- })
97
- }
98
- return aws.fetch(url, options)
99
- }
100
-
101
- interface LambdaError {
102
- readonly errorType: string
103
- readonly errorMessage: string
104
- readonly stackTrace?: string[]
105
- }
106
-
107
- function formatError(error: unknown): LambdaError {
108
- if (error instanceof Error) {
109
- return {
110
- errorType: error.name,
111
- errorMessage: error.message,
112
- stackTrace: error.stack?.split('\n').filter(line => !line.includes(' /opt/runtime.ts')),
113
- }
114
- }
115
- return {
116
- errorType: 'Error',
117
- errorMessage: Bun.inspect(error),
118
- }
119
- }
120
-
121
- async function sendError(type: string, cause: unknown): Promise<void> {
122
- console.error(cause)
123
- await fetch(requestId === undefined ? 'runtime/init/error' : `runtime/invocation/${requestId}/error`, {
124
- method: 'POST',
125
- headers: {
126
- 'Content-Type': 'application/vnd.aws.lambda.error+json',
127
- 'Lambda-Runtime-Function-Error-Type': `Bun.${type}`,
128
- },
129
- body: JSON.stringify(formatError(cause)),
130
- })
131
- }
132
-
133
- async function throwError(type: string, cause: unknown): Promise<never> {
134
- await sendError(type, cause)
135
- exit()
136
- }
137
-
138
- async function init(): Promise<Lambda> {
139
- const handlerName = env('_HANDLER')
140
- const index = handlerName.lastIndexOf('.')
141
- const fileName = handlerName.substring(0, index)
142
- const filePath = `${env('LAMBDA_TASK_ROOT')}/${fileName}`
143
- let file
144
- try {
145
- file = await import(filePath)
146
- }
147
- catch (cause) {
148
- if (cause instanceof Error && cause.message.startsWith('Cannot find module'))
149
- return throwError('FileDoesNotExist', `Did not find a file named '${fileName}'`)
150
-
151
- return throwError('InitError', cause)
152
- }
153
- const moduleName = handlerName.substring(index + 1) || 'fetch'
154
- let module = file.default ?? file[moduleName] ?? {}
155
- if (typeof module === 'function') {
156
- module = {
157
- fetch: module,
158
- }
159
- }
160
- else if (typeof module === 'object' && moduleName !== 'fetch') {
161
- module = {
162
- ...module,
163
- fetch: module[moduleName],
164
- }
165
- }
166
- const { fetch, websocket } = module
167
- if (typeof fetch !== 'function') {
168
- return throwError(
169
- fetch === undefined ? 'MethodDoesNotExist' : 'MethodIsNotAFunction',
170
- `${fileName} does not have a default export with a function named '${moduleName}'`,
171
- )
172
- }
173
- if (websocket === undefined)
174
- return module
175
-
176
- for (const name of ['open', 'message', 'close']) {
177
- const method = websocket[name]
178
- if (method === undefined)
179
- continue
180
-
181
- if (typeof method !== 'function') {
182
- return throwError(
183
- 'MethodIsNotAFunction',
184
- `${fileName} does not have a function named '${name}' on the default 'websocket' property`,
185
- )
186
- }
187
- }
188
- return module
189
- }
190
-
191
- interface LambdaRequest<E = any> {
192
- readonly requestId: string
193
- readonly traceId: string
194
- readonly functionArn: string
195
- readonly deadlineMs: number | null
196
- readonly event: E
197
- }
198
-
199
- async function receiveRequest(): Promise<LambdaRequest> {
200
- const response = await fetch('runtime/invocation/next')
201
- requestId = response.headers.get('Lambda-Runtime-Aws-Request-Id') ?? undefined
202
- if (requestId === undefined)
203
- exit('Runtime received a request without a request ID')
204
-
205
- traceId = response.headers.get('Lambda-Runtime-Trace-Id') ?? undefined
206
- if (traceId === undefined)
207
- exit('Runtime received a request without a trace ID')
208
-
209
- process.env._X_AMZN_TRACE_ID = traceId
210
- functionArn = response.headers.get('Lambda-Runtime-Invoked-Function-Arn') ?? undefined
211
- if (functionArn === undefined)
212
- exit('Runtime received a request without a function ARN')
213
-
214
- const deadlineMs = Number.parseInt(response.headers.get('Lambda-Runtime-Deadline-Ms') ?? '0') || null
215
- let event
216
- try {
217
- event = await response.json()
218
- }
219
- catch (cause) {
220
- exit('Runtime received a request with invalid JSON', cause)
221
- }
222
- return {
223
- requestId,
224
- traceId,
225
- functionArn,
226
- deadlineMs,
227
- event,
228
- }
229
- }
230
-
231
- interface LambdaResponse {
232
- readonly statusCode: number
233
- readonly headers?: Record<string, string>
234
- readonly isBase64Encoded?: boolean
235
- readonly body?: string
236
- readonly multiValueHeaders?: Record<string, string[]>
237
- readonly cookies?: string[]
238
- }
239
-
240
- async function formatResponse(response: Response): Promise<LambdaResponse> {
241
- const statusCode = response.status
242
- const headers = response.headers.toJSON()
243
- if (statusCode === 101) {
244
- const protocol = headers['sec-websocket-protocol']
245
- if (protocol === undefined) {
246
- return {
247
- statusCode: 200,
248
- }
249
- }
250
- return {
251
- statusCode: 200,
252
- headers: {
253
- 'Sec-WebSocket-Protocol': protocol,
254
- },
255
- }
256
- }
257
- const mime = headers['content-type']
258
- const isBase64Encoded = !mime || (!mime.startsWith('text/') && !mime.startsWith('application/json'))
259
- const body = isBase64Encoded ? Buffer.from(await response.arrayBuffer()).toString('base64') : await response.text()
260
- delete headers['set-cookie']
261
- const cookies = response.headers.getAll('Set-Cookie')
262
- if (cookies.length === 0) {
263
- return {
264
- statusCode,
265
- headers,
266
- isBase64Encoded,
267
- body,
268
- }
269
- }
270
- return {
271
- statusCode,
272
- headers,
273
- cookies,
274
- multiValueHeaders: {
275
- 'Set-Cookie': cookies,
276
- },
277
- isBase64Encoded,
278
- body,
279
- }
280
- }
281
-
282
- async function sendResponse(response: unknown): Promise<void> {
283
- if (requestId === undefined)
284
- exit('Runtime attempted to send a response without a request ID')
285
-
286
- await fetch(`runtime/invocation/${requestId}/response`, {
287
- method: 'POST',
288
- body: response === null ? null : JSON.stringify(response),
289
- })
290
- }
291
-
292
- function formatBody(body?: string, isBase64Encoded?: boolean): string | null {
293
- if (body === undefined)
294
- return null
295
-
296
- if (!isBase64Encoded)
297
- return body
298
-
299
- return Buffer.from(body).toString('base64')
300
- }
301
-
302
- interface HttpEventV1 {
303
- readonly requestContext: {
304
- readonly requestId: string
305
- readonly domainName: string
306
- readonly httpMethod: string
307
- readonly path: string
308
- }
309
- readonly headers: Record<string, string>
310
- readonly multiValueHeaders?: Record<string, string[]>
311
- readonly queryStringParameters?: Record<string, string>
312
- readonly multiValueQueryStringParameters?: Record<string, string[]>
313
- readonly isBase64Encoded: boolean
314
- readonly body?: string
315
- }
316
-
317
- function isHttpEventV1(event: any): event is HttpEventV1 {
318
- return !event.Records && event.version !== '2.0' && event.version !== '0' && typeof event.requestContext === 'object'
319
- }
320
-
321
- function formatHttpEventV1(event: HttpEventV1): Request {
322
- const request = event.requestContext
323
- const headers = new Headers()
324
- for (const [name, values] of Object.entries(event.multiValueHeaders ?? {})) {
325
- for (const value of values)
326
- headers.append(name, value)
327
- }
328
- const hostname = headers.get('Host') ?? request.domainName
329
- const proto = headers.get('X-Forwarded-Proto') ?? 'http'
330
- const url = new URL(request.path, `${proto}://${hostname}/`)
331
- for (const [name, values] of Object.entries(event.multiValueQueryStringParameters ?? {})) {
332
- for (const value of values ?? [])
333
- url.searchParams.append(name, value)
334
- }
335
- return new Request(url.toString(), {
336
- method: request.httpMethod,
337
- headers,
338
- body: formatBody(event.body, event.isBase64Encoded),
339
- })
340
- }
341
-
342
- interface HttpEventV2 {
343
- readonly version: '2.0'
344
- readonly requestContext: {
345
- readonly requestId: string
346
- readonly domainName: string
347
- readonly http: {
348
- readonly method: string
349
- readonly path: string
350
- }
351
- }
352
- readonly headers: Record<string, string>
353
- readonly queryStringParameters?: Record<string, string>
354
- readonly cookies?: string[]
355
- readonly isBase64Encoded: boolean
356
- readonly body?: string
357
- }
358
-
359
- function isHttpEventV2(event: any): event is HttpEventV2 {
360
- return !event.Records && event.version === '2.0' && typeof event.requestContext === 'object'
361
- }
362
-
363
- function formatHttpEventV2(event: HttpEventV2): Request {
364
- const request = event.requestContext
365
- const headers = new Headers()
366
- for (const [name, values] of Object.entries(event.headers)) {
367
- for (const value of values.split(','))
368
- headers.append(name, value)
369
- }
370
- for (const [name, values] of Object.entries(event.queryStringParameters ?? {})) {
371
- for (const value of values.split(','))
372
- headers.append(name, value)
373
- }
374
- for (const cookie of event.cookies ?? [])
375
- headers.append('Set-Cookie', cookie)
376
-
377
- const hostname = headers.get('Host') ?? request.domainName
378
- const proto = headers.get('X-Forwarded-Proto') ?? 'http'
379
- const url = new URL(request.http.path, `${proto}://${hostname}/`)
380
- return new Request(url.toString(), {
381
- method: request.http.method,
382
- headers,
383
- body: formatBody(event.body, event.isBase64Encoded),
384
- })
385
- }
386
-
387
- function isHttpEvent(event: any): boolean {
388
- return isHttpEventV1(event) || isHttpEventV2(event)
389
- }
390
-
391
- interface WebSocketEvent {
392
- readonly headers: Record<string, string>
393
- readonly multiValueHeaders: Record<string, string[]>
394
- readonly isBase64Encoded: boolean
395
- readonly body?: string
396
- readonly requestContext: {
397
- readonly apiId: string
398
- readonly requestId: string
399
- readonly connectionId: string
400
- readonly domainName: string
401
- readonly stage: string
402
- readonly identity: {
403
- readonly sourceIp: string
404
- }
405
- } & (
406
- | {
407
- readonly eventType: 'CONNECT'
408
- }
409
- | {
410
- readonly eventType: 'MESSAGE'
411
- }
412
- | {
413
- readonly eventType: 'DISCONNECT'
414
- readonly disconnectStatusCode: number
415
- readonly disconnectReason: string
416
- }
417
- )
418
- }
419
-
420
- function isWebSocketEvent(event: any): event is WebSocketEvent {
421
- return typeof event.requestContext === 'object' && typeof event.requestContext.connectionId === 'string'
422
- }
423
-
424
- function isWebSocketUpgrade(event: any): event is WebSocketEvent {
425
- return isWebSocketEvent(event) && event.requestContext.eventType === 'CONNECT'
426
- }
427
-
428
- function formatWebSocketUpgrade(event: WebSocketEvent): Request {
429
- const request = event.requestContext
430
- const headers = new Headers()
431
- headers.set('Upgrade', 'websocket')
432
- headers.set('x-amzn-connection-id', request.connectionId)
433
- for (const [name, values] of Object.entries(event.multiValueHeaders as any)) {
434
- for (const value of (values as any) ?? [])
435
- headers.append(name, value)
436
- }
437
- const hostname = headers.get('Host') ?? request.domainName
438
- const proto = headers.get('X-Forwarded-Proto') ?? 'http'
439
- const url = new URL(`${proto}://${hostname}/${request.stage}`)
440
- return new Request(url.toString(), {
441
- headers,
442
- body: formatBody(event.body, event.isBase64Encoded),
443
- })
444
- }
445
-
446
- function formatUnknownEvent(event: unknown): Request {
447
- return new Request('https://lambda/', {
448
- method: 'POST',
449
- body: JSON.stringify(event),
450
- headers: {
451
- 'Content-Type': 'application/json;charset=utf-8',
452
- },
453
- })
454
- }
455
-
456
- function formatRequest(input: LambdaRequest): Request | undefined {
457
- const { event, requestId, traceId, functionArn, deadlineMs } = input
458
- let request: Request
459
- if (isHttpEventV2(event)) {
460
- request = formatHttpEventV2(event)
461
- }
462
- else if (isHttpEventV1(event)) {
463
- request = formatHttpEventV1(event)
464
- }
465
- else if (isWebSocketEvent(event)) {
466
- if (!isWebSocketUpgrade(event))
467
- return undefined
468
-
469
- request = formatWebSocketUpgrade(event)
470
- }
471
- else {
472
- request = formatUnknownEvent(input)
473
- }
474
- request.headers.set('x-amzn-requestid', requestId)
475
- request.headers.set('x-amzn-trace-id', traceId)
476
- request.headers.set('x-amzn-function-arn', functionArn)
477
- if (deadlineMs !== null)
478
- request.headers.set('x-amzn-deadline-ms', `${deadlineMs}`)
479
-
480
- // @ts-expect-error: Attach the original event to the Request
481
- request.aws = event
482
- return request
483
- }
484
-
485
- class LambdaServer implements Server {
486
- #lambda: Lambda
487
- #webSockets: Map<string, LambdaWebSocket>
488
- #upgrade: Response | null
489
- pendingRequests: number
490
- pendingWebSockets: number
491
- port: number
492
- hostname: string
493
- development: boolean
494
-
495
- constructor(lambda: Lambda) {
496
- this.#lambda = lambda
497
- this.#webSockets = new Map()
498
- this.#upgrade = null
499
- this.pendingRequests = 0
500
- this.pendingWebSockets = 0
501
- this.port = 80
502
- this.hostname = 'lambda'
503
- this.development = false
504
- }
505
-
506
- async accept(request: LambdaRequest): Promise<unknown> {
507
- const deadlineMs = request.deadlineMs === null ? Date.now() + 60_000 : request.deadlineMs
508
- const durationMs = Math.max(1, deadlineMs - Date.now())
509
- let response: unknown
510
- try {
511
- response = await Promise.race([
512
- new Promise<undefined>(resolve => setTimeout(resolve, durationMs)),
513
- this.#acceptRequest(request),
514
- ])
515
- }
516
- catch (cause) {
517
- await sendError('RequestError', cause)
518
- return
519
- }
520
- if (response === undefined) {
521
- await sendError('TimeoutError', 'Function timed out')
522
- return
523
- }
524
- return response
525
- }
526
-
527
- async #acceptRequest(event: LambdaRequest): Promise<unknown> {
528
- const request = formatRequest(event)
529
- let response: Response | undefined
530
- if (request === undefined) {
531
- await this.#acceptWebSocket(event.event)
532
- }
533
- else {
534
- response = await this.fetch(request)
535
- if (response.status === 101)
536
- await this.#acceptWebSocket(event.event)
537
- }
538
- if (response === undefined) {
539
- return {
540
- statusCode: 200,
541
- }
542
- }
543
- if (!isHttpEvent(event.event))
544
- return response.text()
545
-
546
- return formatResponse(response)
547
- }
548
-
549
- async #acceptWebSocket(event: WebSocketEvent): Promise<void> {
550
- const request = event.requestContext
551
- const { connectionId, eventType } = request
552
- const webSocket = this.#webSockets.get(connectionId)
553
- if (webSocket === undefined || this.#lambda.websocket === undefined)
554
- return
555
-
556
- const { open, message, close } = this.#lambda.websocket
557
- switch (eventType) {
558
- case 'CONNECT': {
559
- if (open)
560
- await open(webSocket)
561
-
562
- break
563
- }
564
- case 'MESSAGE': {
565
- if (message) {
566
- const body = formatBody(event.body, event.isBase64Encoded)
567
- if (body !== null)
568
- await message(webSocket, body)
569
- }
570
- break
571
- }
572
- case 'DISCONNECT': {
573
- try {
574
- if (close) {
575
- const { disconnectStatusCode: code, disconnectReason: reason } = request
576
- await close(webSocket, code, reason)
577
- }
578
- }
579
- finally {
580
- this.#webSockets.delete(connectionId)
581
- this.pendingWebSockets--
582
- }
583
- break
584
- }
585
- }
586
- }
587
-
588
- stop(): void {
589
- exit('Runtime exited because Server.stop() was called')
590
- }
591
-
592
- reload(options: any): void {
593
- this.#lambda = {
594
- fetch: options.fetch ?? this.#lambda.fetch,
595
- error: options.error ?? this.#lambda.error,
596
- websocket: options.websocket ?? this.#lambda.websocket,
597
- }
598
- this.port
599
- = typeof options.port === 'number'
600
- ? options.port
601
- : typeof options.port === 'string'
602
- ? Number.parseInt(options.port)
603
- : this.port
604
- this.hostname = options.hostname ?? this.hostname
605
- this.development = options.development ?? this.development
606
- }
607
-
608
- async fetch(request: Request): Promise<Response> {
609
- this.pendingRequests++
610
- try {
611
- const response = await this.#lambda.fetch(request, this)
612
- if (response instanceof Response)
613
- return response
614
-
615
- if (response === undefined && this.#upgrade !== null)
616
- return this.#upgrade
617
-
618
- throw new Error('fetch() did not return a Response')
619
- }
620
- catch (cause) {
621
- console.error(cause)
622
- if (this.#lambda.error !== undefined) {
623
- try {
624
- return await this.#lambda.error(cause)
625
- }
626
- catch (cause) {
627
- console.error(cause)
628
- }
629
- }
630
- return new Response(null, { status: 500 })
631
- }
632
- finally {
633
- this.pendingRequests--
634
- this.#upgrade = null
635
- }
636
- }
637
-
638
- upgrade<T = undefined>(
639
- request: Request,
640
- options?: {
641
- headers?: HeadersInit
642
- data?: T
643
- },
644
- ): boolean {
645
- if (request.method === 'GET' && request.headers.get('Upgrade')?.toLowerCase() === 'websocket') {
646
- this.#upgrade = new Response(null, {
647
- status: 101,
648
- headers: options?.headers,
649
- })
650
- if ('aws' in request && isWebSocketUpgrade(request.aws)) {
651
- const { connectionId } = request.aws.requestContext
652
- this.#webSockets.set(connectionId, new LambdaWebSocket(request.aws, options?.data))
653
- this.pendingWebSockets++
654
- }
655
- return true
656
- }
657
- return false
658
- }
659
-
660
- publish(topic: string, data: string | ArrayBuffer | ArrayBufferView, compress?: boolean): number {
661
- let count = 0
662
- for (const webSocket of this.#webSockets.values())
663
- count += webSocket.publish(topic, data, compress) ? 1 : 0
664
-
665
- return count
666
- }
667
- }
668
-
669
- class LambdaWebSocket implements ServerWebSocket {
670
- #connectionId: string
671
- #url: string
672
- #invokeArn: string
673
- #topics: Set<string> | null
674
- remoteAddress: string
675
- readyState: 0 | 2 | 1 | -1 | 3
676
- binaryType?: 'arraybuffer' | 'uint8array'
677
- data: any
678
-
679
- constructor(event: WebSocketEvent, data?: any) {
680
- const request = event.requestContext
681
- this.#connectionId = `${request.connectionId}`
682
- this.#url = `https://${request.domainName}/${request.stage}/@connections/${this.#connectionId}`
683
- const [region, accountId] = (functionArn ?? '').split(':').slice(3, 5)
684
- this.#invokeArn = `arn:aws:execute-api:${region}:${accountId}:${request.apiId}/${request.stage}/*`
685
- this.#topics = null
686
- this.remoteAddress = request.identity.sourceIp
687
- this.readyState = 1 // WebSocket.OPEN
688
- this.data = data
689
- }
690
-
691
- send(data: string | ArrayBuffer | ArrayBufferView, compress?: boolean): number {
692
- if (typeof data === 'string')
693
- return this.sendText(data, compress)
694
-
695
- if (data instanceof ArrayBuffer)
696
- return this.sendBinary(new Uint8Array(data), compress)
697
-
698
- const buffer = new Uint8Array(data.buffer, data.byteOffset, data.byteLength)
699
- return this.sendBinary(buffer, compress)
700
- }
701
-
702
- sendText(data: string, compress?: boolean): number {
703
- fetchAws(this.#url, {
704
- method: 'POST',
705
- body: data,
706
- })
707
- .then(({ status }) => {
708
- if (status === 403) {
709
- warnOnce(
710
- 'Failed to send WebSocket message due to insufficient IAM permissions',
711
- `Assign the following IAM policy to ${functionArn} to fix this issue:`,
712
- {
713
- Version: '2012-10-17',
714
- Statement: [
715
- {
716
- Effect: 'Allow',
717
- Action: ['execute-api:Invoke'],
718
- Resource: [this.#invokeArn],
719
- },
720
- ],
721
- },
722
- )
723
- }
724
- else {
725
- warnOnce(`Failed to send WebSocket message due to a ${status} error`)
726
- }
727
- })
728
- .catch((error) => {
729
- warnOnce('Failed to send WebSocket message', error)
730
- })
731
- return data.length
732
- }
733
-
734
- sendBinary(data: Uint8Array, compress?: boolean): number {
735
- warnOnce(
736
- 'Lambda does not support binary WebSocket messages',
737
- 'https://docs.aws.amazon.com/apigateway/latest/developerguide/websocket-api-develop-binary-media-types.html',
738
- )
739
- const base64 = Buffer.from(data).toString('base64')
740
- return this.sendText(base64, compress)
741
- }
742
-
743
- publish(topic: string, data: string | ArrayBuffer | ArrayBufferView, compress?: boolean): number {
744
- if (this.isSubscribed(topic))
745
- return this.send(data, compress)
746
-
747
- return -1
748
- }
749
-
750
- publishText(topic: string, data: string, compress?: boolean): number {
751
- if (this.isSubscribed(topic))
752
- return this.sendText(data, compress)
753
-
754
- return -1
755
- }
756
-
757
- publishBinary(topic: string, data: Uint8Array, compress?: boolean): number {
758
- if (this.isSubscribed(topic))
759
- return this.sendBinary(data, compress)
760
-
761
- return -1
762
- }
763
-
764
- close(code?: number, reason?: string): void {
765
- // TODO: code? reason?
766
- fetchAws(this.#url, {
767
- method: 'DELETE',
768
- })
769
- .then(({ status }) => {
770
- if (status === 403) {
771
- warnOnce(
772
- 'Failed to close WebSocket due to insufficient IAM permissions',
773
- `Assign the following IAM policy to ${functionArn} to fix this issue:`,
774
- {
775
- Version: '2012-10-17',
776
- Statement: [
777
- {
778
- Effect: 'Allow',
779
- Action: ['execute-api:Invoke'],
780
- Resource: [this.#invokeArn],
781
- },
782
- ],
783
- },
784
- )
785
- }
786
- else {
787
- warnOnce(`Failed to close WebSocket due to a ${status} error`)
788
- }
789
- })
790
- .catch((error) => {
791
- warnOnce('Failed to close WebSocket', error)
792
- })
793
- this.readyState = 3 // WebSocket.CLOSED;
794
- }
795
-
796
- subscribe(topic: string): void {
797
- if (this.#topics === null)
798
- this.#topics = new Set()
799
-
800
- this.#topics.add(topic)
801
- }
802
-
803
- unsubscribe(topic: string): void {
804
- if (this.#topics !== null)
805
- this.#topics.delete(topic)
806
- }
807
-
808
- isSubscribed(topic: string): boolean {
809
- return this.#topics !== null && this.#topics.has(topic)
810
- }
811
-
812
- cork(callback: (ws: ServerWebSocket<undefined>) => any): void | Promise<void> {
813
- // Lambda does not support sending multiple messages at a time.
814
- return callback(this)
815
- }
816
- }
817
-
818
- const lambda = await init()
819
- const server = new LambdaServer(lambda)
820
- while (true) {
821
- try {
822
- const request = await receiveRequest()
823
- const response = await server.accept(request)
824
- if (response !== undefined)
825
- await sendResponse(response)
826
- }
827
- finally {
828
- reset()
829
- }
830
- }