@stacksjs/bun-router 0.0.15 → 0.0.17

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 (83) hide show
  1. package/dist/auth.d.ts +22 -8
  2. package/dist/chunk-1ahs68ys.js +18 -0
  3. package/dist/{chunk-j0e7z7hd.js → chunk-cgptvjdf.js} +153 -75
  4. package/dist/chunk-g3ybefhg.js +1923 -0
  5. package/dist/cli.js +13 -11
  6. package/dist/container/container.d.ts +1 -0
  7. package/dist/container/contextual-binding.d.ts +0 -1
  8. package/dist/container/decorators.d.ts +2 -5
  9. package/dist/container/index.d.ts +16 -0
  10. package/dist/container/index.js +322 -0
  11. package/dist/file-serving/static-files.d.ts +16 -0
  12. package/dist/index.d.ts +17 -1
  13. package/dist/index.js +1086 -1270
  14. package/dist/middleware/cors.d.ts +9 -0
  15. package/dist/middleware/csrf.d.ts +7 -0
  16. package/dist/middleware/ddos_protection.d.ts +16 -1
  17. package/dist/middleware/rate_limit.d.ts +7 -1
  18. package/dist/middleware/request_tracer.d.ts +15 -9
  19. package/dist/middleware/response_cache.d.ts +2 -1
  20. package/dist/request/context.d.ts +12 -1
  21. package/dist/request/macros.d.ts +40 -2
  22. package/dist/response/macros.d.ts +3 -3
  23. package/dist/router/fluent-routing.d.ts +50 -13
  24. package/dist/router/handler-resolver.d.ts +8 -0
  25. package/dist/router/index.d.ts +2 -0
  26. package/dist/router/route-compiler.d.ts +8 -10
  27. package/dist/router/route-trie.d.ts +7 -1
  28. package/dist/router/router.d.ts +13 -2
  29. package/dist/router/validation-integration.d.ts +10 -4
  30. package/dist/types/middleware-types.d.ts +1 -1
  31. package/dist/types/route-inference.d.ts +9 -2
  32. package/dist/types.d.ts +62 -9
  33. package/dist/utils.d.ts +1 -1
  34. package/package.json +11 -1
  35. package/src/auth.ts +32 -12
  36. package/src/container/container.ts +58 -12
  37. package/src/container/contextual-binding.ts +4 -1
  38. package/src/container/decorators.ts +16 -7
  39. package/src/container/index.ts +49 -0
  40. package/src/errors/circuit-breaker.ts +3 -2
  41. package/src/errors/graceful-degradation.ts +2 -0
  42. package/src/file-serving/static-files.ts +144 -7
  43. package/src/index.ts +53 -1
  44. package/src/middleware/cors.ts +76 -8
  45. package/src/middleware/csrf.ts +80 -22
  46. package/src/middleware/ddos_protection.ts +27 -3
  47. package/src/middleware/input_validation.ts +10 -6
  48. package/src/middleware/performance_alerting.ts +2 -0
  49. package/src/middleware/performance_monitor.ts +2 -0
  50. package/src/middleware/rate_limit.ts +7 -1
  51. package/src/middleware/request_signing.ts +2 -0
  52. package/src/middleware/request_tracer.ts +34 -17
  53. package/src/middleware/response_cache.ts +48 -13
  54. package/src/middleware/session.ts +3 -3
  55. package/src/observability/metrics.ts +1 -1
  56. package/src/request/context.ts +50 -3
  57. package/src/request/enhanced-request.ts +20 -1
  58. package/src/request/macros.ts +183 -49
  59. package/src/response/macros.ts +3 -3
  60. package/src/router/file-based-routing.ts +17 -10
  61. package/src/router/fluent-routing.ts +117 -48
  62. package/src/router/group-organization.ts +17 -1
  63. package/src/router/handler-resolver.ts +36 -0
  64. package/src/router/http-methods.ts +33 -3
  65. package/src/router/index.ts +6 -0
  66. package/src/router/middleware.ts +3 -0
  67. package/src/router/optimized-route-matching.ts +58 -9
  68. package/src/router/route-compiler.ts +62 -73
  69. package/src/router/route-matching.ts +19 -0
  70. package/src/router/route-trie.ts +34 -48
  71. package/src/router/router.ts +72 -60
  72. package/src/router/server.ts +361 -139
  73. package/src/router/validation-integration.ts +11 -5
  74. package/src/testing/auth-testing.ts +6 -4
  75. package/src/types/middleware-types.ts +7 -4
  76. package/src/types/route-inference.ts +29 -14
  77. package/src/types.ts +63 -7
  78. package/src/url.ts +7 -2
  79. package/src/utils.ts +107 -39
  80. package/src/validation/validator.ts +7 -4
  81. package/src/websocket/clustering.ts +8 -1
  82. package/dist/router/fluent-router.d.ts +0 -315
  83. package/src/router/fluent-router.ts +0 -927
@@ -1,3 +1,4 @@
1
+ import type { StorageProvider } from 'ts-rate-limiter'
1
2
  import type { EnhancedRequest, Middleware, MiddlewareHandler, NextFunction } from '../types'
2
3
  import { createRateLimiter } from 'ts-rate-limiter'
3
4
  import { config } from '../config'
@@ -11,7 +12,12 @@ export interface RateLimitOptions {
11
12
  keyGenerator?: (request: Request) => string | Promise<string>
12
13
  handler?: (request: Request, limit: { remaining: number, limit: number, resetTime: number }) => Response | Promise<Response>
13
14
  skip?: (request: Request) => boolean | Promise<boolean>
14
- storage?: any
15
+ /**
16
+ * Custom storage backend (ts-rate-limiter `StorageProvider`), or the
17
+ * string `'redis'` to use the built-in Redis store configured via
18
+ * the `redis` option.
19
+ */
20
+ storage?: StorageProvider | 'redis'
15
21
  algorithm?: 'fixed-window' | 'sliding-window' | 'token-bucket'
16
22
  draftMode?: boolean
17
23
  redis?: {
@@ -149,6 +149,8 @@ export class InMemoryNonceStore implements NonceStore {
149
149
 
150
150
  constructor(cleanupIntervalMs: number = 60000) {
151
151
  this.cleanupInterval = setInterval(() => this.cleanup(), cleanupIntervalMs)
152
+ // Background housekeeping must not keep the process alive
153
+ this.cleanupInterval.unref?.()
152
154
  }
153
155
 
154
156
  has(nonce: string): boolean {
@@ -9,18 +9,30 @@ export interface TraceSpan {
9
9
  startTime: number
10
10
  endTime?: number
11
11
  duration?: number
12
- tags: Record<string, any>
12
+ tags: Record<string, unknown>
13
13
  logs: Array<{
14
14
  timestamp: number
15
15
  level: 'debug' | 'info' | 'warn' | 'error'
16
16
  message: string
17
- fields?: Record<string, any>
17
+ fields?: Record<string, unknown>
18
18
  }>
19
19
  status: 'ok' | 'error' | 'timeout'
20
20
  error?: string
21
21
  stackTrace?: string[]
22
22
  }
23
23
 
24
+ /**
25
+ * Destination for exported spans. `endpoint`/`headers` configure the
26
+ * HTTP-based exporters (jaeger/zipkin/otlp); `customExporter` receives
27
+ * the spans directly.
28
+ */
29
+ export interface TraceExporterConfig {
30
+ type: 'console' | 'jaeger' | 'zipkin' | 'otlp' | 'custom'
31
+ endpoint?: string
32
+ headers?: Record<string, string>
33
+ customExporter?: (spans: TraceSpan[]) => Promise<void>
34
+ }
35
+
24
36
  export interface TracingOptions {
25
37
  enabled?: boolean
26
38
  sampleRate?: number
@@ -30,12 +42,7 @@ export interface TracingOptions {
30
42
  includeRequestBody?: boolean
31
43
  includeResponseBody?: boolean
32
44
  maxBodySize?: number
33
- exporters?: Array<{
34
- type: 'console' | 'jaeger' | 'zipkin' | 'otlp' | 'custom'
35
- endpoint?: string
36
- headers?: Record<string, string>
37
- customExporter?: (spans: TraceSpan[]) => Promise<void>
38
- }>
45
+ exporters?: TraceExporterConfig[]
39
46
  propagation?: {
40
47
  enabled?: boolean
41
48
  headers?: string[]
@@ -83,6 +90,8 @@ export default class RequestTracer {
83
90
  console.warn('Error exporting spans:', error)
84
91
  }
85
92
  }, 100) // Export every 100ms for faster testing
93
+ // Background export must not keep the process alive
94
+ this.exportInterval.unref?.()
86
95
  }
87
96
 
88
97
  private shouldSample(): boolean {
@@ -120,7 +129,7 @@ export default class RequestTracer {
120
129
  return { traceId, parentSpanId }
121
130
  }
122
131
 
123
- private async captureRequestBody(req: EnhancedRequest): Promise<any> {
132
+ private async captureRequestBody(req: EnhancedRequest): Promise<unknown> {
124
133
  if (!this.options.includeRequestBody)
125
134
  return undefined
126
135
 
@@ -157,7 +166,7 @@ export default class RequestTracer {
157
166
  return undefined
158
167
  }
159
168
 
160
- private async captureResponseBody(response: Response): Promise<any> {
169
+ private async captureResponseBody(response: Response): Promise<unknown> {
161
170
  if (!this.options.includeResponseBody)
162
171
  return undefined
163
172
 
@@ -203,7 +212,7 @@ export default class RequestTracer {
203
212
  }
204
213
  }
205
214
 
206
- private addSpanTag(spanId: string, key: string, value: any): void {
215
+ private addSpanTag(spanId: string, key: string, value: unknown): void {
207
216
  const span = this.spans.get(spanId)
208
217
  if (span) {
209
218
  span.tags[key] = value
@@ -247,15 +256,23 @@ export default class RequestTracer {
247
256
  }
248
257
 
249
258
  private async exportSpans(): Promise<void> {
250
- // For debugging
251
- console.warn(`[RequestTracer] Exporting spans, queue size: ${this.exportQueue.length}, active spans: ${this.spans.size}`)
259
+ // Drop orphaned spans: anything unfinished for over a minute will
260
+ // never be completed (its request died mid-flight) and would pin the
261
+ // map forever
262
+ if (this.spans.size > 0) {
263
+ const cutoff = performance.now() - 60_000
264
+ for (const [spanId, span] of this.spans) {
265
+ if (span.endTime === undefined && span.startTime < cutoff) {
266
+ this.spans.delete(spanId)
267
+ }
268
+ }
269
+ }
252
270
 
253
271
  if (this.exportQueue.length === 0) {
254
272
  // Also check if there are any finished spans that need to be exported
255
273
  // This ensures spans are exported even if they weren't properly moved to the queue
256
274
  const finishedSpans = Array.from(this.spans.values()).filter(span => span.endTime !== undefined)
257
275
  if (finishedSpans.length > 0) {
258
- console.warn(`[RequestTracer] Found ${finishedSpans.length} finished spans to export`)
259
276
  // Move finished spans to export queue
260
277
  this.exportQueue.push(...finishedSpans)
261
278
  // Remove them from the spans map
@@ -323,7 +340,7 @@ export default class RequestTracer {
323
340
  }
324
341
  }
325
342
 
326
- private async exportToJaeger(spans: TraceSpan[], exporter: any): Promise<void> {
343
+ private async exportToJaeger(spans: TraceSpan[], exporter: TraceExporterConfig): Promise<void> {
327
344
  if (!exporter.endpoint)
328
345
  return
329
346
 
@@ -366,7 +383,7 @@ export default class RequestTracer {
366
383
  })
367
384
  }
368
385
 
369
- private async exportToZipkin(spans: TraceSpan[], exporter: any): Promise<void> {
386
+ private async exportToZipkin(spans: TraceSpan[], exporter: TraceExporterConfig): Promise<void> {
370
387
  if (!exporter.endpoint)
371
388
  return
372
389
 
@@ -590,7 +607,7 @@ export default class RequestTracer {
590
607
  return childSpanId
591
608
  }
592
609
 
593
- addTag(spanId: string, key: string, value: any): void {
610
+ addTag(spanId: string, key: string, value: unknown): void {
594
611
  this.addSpanTag(spanId, key, value)
595
612
  }
596
613
 
@@ -9,7 +9,9 @@ export interface CacheEntry {
9
9
  status: number
10
10
  statusText: string
11
11
  headers: Record<string, string>
12
- body: number[] // Store as number array for JSON serialization
12
+ // In-memory entries hold the raw bytes; entries round-tripped through
13
+ // the JSON file cache come back as a plain number array
14
+ body: Uint8Array | number[]
13
15
  contentType: string
14
16
  }
15
17
  createdAt: number
@@ -73,6 +75,7 @@ export class ResponseCache implements Middleware {
73
75
  }
74
76
 
75
77
  private cleanupInterval?: ReturnType<typeof setInterval>
78
+ private compiledInvalidationPatterns: RegExp[] = []
76
79
 
77
80
  constructor(options: ResponseCacheOptions = {}) {
78
81
  this.options = {
@@ -121,6 +124,20 @@ export class ResponseCache implements Middleware {
121
124
  },
122
125
  } as Required<ResponseCacheOptions>
123
126
 
127
+ // Compile invalidation patterns once — matching per request with
128
+ // `new RegExp(pattern)` recompiled the regex on every invalidation check
129
+ this.compiledInvalidationPatterns = (this.options.invalidation.patterns ?? [])
130
+ .map((pattern) => {
131
+ try {
132
+ return new RegExp(pattern)
133
+ }
134
+ catch {
135
+ console.warn(`[ResponseCache] Invalid invalidation pattern: ${pattern}`)
136
+ return null
137
+ }
138
+ })
139
+ .filter((re): re is RegExp => re !== null)
140
+
124
141
  this.initializeStorage()
125
142
  this.startCleanupInterval()
126
143
  }
@@ -142,6 +159,8 @@ export class ResponseCache implements Middleware {
142
159
  this.cleanupInterval = setInterval(() => {
143
160
  void this.cleanup()
144
161
  }, 60000) // Cleanup every minute
162
+ // Background housekeeping must not keep the process alive
163
+ this.cleanupInterval.unref?.()
145
164
  }
146
165
 
147
166
  public async cleanup(): Promise<void> {
@@ -370,7 +389,12 @@ export class ResponseCache implements Middleware {
370
389
 
371
390
  try {
372
391
  const filePath = join(this.options.storage.directory, `${key}.cache`)
373
- const content = JSON.stringify(entry)
392
+ // Uint8Array doesn't JSON-serialize as an array — convert at the
393
+ // file boundary only (memory entries keep the compact byte view)
394
+ const serializable = entry.response.body instanceof Uint8Array
395
+ ? { ...entry, response: { ...entry.response, body: Array.from(entry.response.body) } }
396
+ : entry
397
+ const content = JSON.stringify(serializable)
374
398
  await Bun.write(filePath, content)
375
399
  }
376
400
  catch (error) {
@@ -574,9 +598,11 @@ export class ResponseCache implements Middleware {
574
598
  }
575
599
 
576
600
  private async createCacheEntry(req: EnhancedRequest, res: Response): Promise<{ entry: CacheEntry, response: Response }> {
577
- // Clone the response to avoid consuming the original stream
578
- const clonedRes = res.clone()
579
- const body = await clonedRes.arrayBuffer()
601
+ // Read the body once and rebuild the outgoing response from the same
602
+ // bytes — cheaper than clone(), which tees the stream and buffers it
603
+ // twice. Bytes are kept as a Uint8Array (the old number[] conversion
604
+ // boxed every byte into a JS number array element).
605
+ const body = await res.arrayBuffer()
580
606
  const bodyBytes = new Uint8Array(body)
581
607
  const ttl = this.getTTL(req)
582
608
  const now = Date.now()
@@ -587,7 +613,7 @@ export class ResponseCache implements Middleware {
587
613
  status: res.status,
588
614
  statusText: res.statusText,
589
615
  headers: Object.fromEntries(res.headers.entries()),
590
- body: Array.from(bodyBytes), // Convert to regular array for JSON serialization
616
+ body: bodyBytes,
591
617
  contentType: res.headers.get('Content-Type') || 'text/plain',
592
618
  },
593
619
  createdAt: now,
@@ -595,21 +621,31 @@ export class ResponseCache implements Middleware {
595
621
  size: bodyBytes.length,
596
622
  }
597
623
 
624
+ const outHeaders = new Headers(res.headers)
625
+
598
626
  // Generate ETag if enabled
599
627
  if (this.options.etag && this.options.etag.enabled) {
600
628
  entry.etag = this.generateETag(bodyBytes)
601
629
  if (entry.etag) {
602
630
  entry.response.headers.ETag = entry.etag
603
- res.headers.set('ETag', entry.etag)
631
+ outHeaders.set('ETag', entry.etag)
604
632
  }
605
633
  }
606
634
 
607
635
  // Set Last-Modified
608
636
  entry.lastModified = new Date(now).toUTCString()
609
637
  entry.response.headers['Last-Modified'] = entry.lastModified
610
- res.headers.set('Last-Modified', entry.lastModified)
638
+ outHeaders.set('Last-Modified', entry.lastModified)
611
639
 
612
- return { entry, response: res }
640
+ const outBody = res.status === 204 || res.status === 304 ? null : bodyBytes
641
+ return {
642
+ entry,
643
+ response: new Response(outBody, {
644
+ status: res.status,
645
+ statusText: res.statusText,
646
+ headers: outHeaders,
647
+ }),
648
+ }
613
649
  }
614
650
 
615
651
  private createResponseFromEntry(entry: CacheEntry): Response {
@@ -645,13 +681,12 @@ export class ResponseCache implements Middleware {
645
681
 
646
682
  async invalidateCache(req?: EnhancedRequest): Promise<void> {
647
683
  if (req) {
648
- const patterns = this.options.invalidation.patterns
649
- if (patterns && patterns.length > 0) {
684
+ if (this.compiledInvalidationPatterns.length > 0) {
650
685
  // Invalidate specific patterns
651
686
  const url = new URL(req.url)
652
687
 
653
- for (const pattern of patterns) {
654
- if (url.pathname.match(new RegExp(pattern))) {
688
+ for (const pattern of this.compiledInvalidationPatterns) {
689
+ if (pattern.test(url.pathname)) {
655
690
  await this.clearCache()
656
691
  return
657
692
  }
@@ -1,4 +1,4 @@
1
- import type { EnhancedRequest, NextFunction } from '../types'
1
+ import type { CookieOptions, EnhancedRequest, NextFunction } from '../types'
2
2
  import type { SessionStore, SessionData } from '../types/core'
3
3
  import { config } from '../config'
4
4
  import { MemorySessionStore } from '../session/memory-store'
@@ -49,7 +49,7 @@ export default class Session {
49
49
 
50
50
  // Create session manager and attach to request
51
51
  const manager = new SessionManager(sessionId, sessionData, this.store)
52
- req.session = manager as any
52
+ req.session = manager
53
53
 
54
54
  // Continue to next middleware/handler
55
55
  const response = await next()
@@ -58,7 +58,7 @@ export default class Session {
58
58
  await manager.save()
59
59
 
60
60
  // Set session cookie on the response
61
- const cookieOptions: any = {
61
+ const cookieOptions: CookieOptions = {
62
62
  maxAge: sessionConfig?.cookie?.maxAge || 86400000,
63
63
  path: sessionConfig?.cookie?.path || '/',
64
64
  }
@@ -520,7 +520,7 @@ export class MetricsRegistry {
520
520
  if (this.config.collectInterval && this.config.collectInterval > 0) {
521
521
  setInterval(() => {
522
522
  this.collectSystemMetrics()
523
- }, this.config.collectInterval)
523
+ }, this.config.collectInterval).unref?.()
524
524
  }
525
525
  }
526
526
 
@@ -17,12 +17,45 @@ interface RequestContextStore {
17
17
 
18
18
  const storage: AsyncLocalStorage<RequestContextStore> = new AsyncLocalStorage<RequestContextStore>()
19
19
 
20
+ // AsyncLocalStorage.run costs roughly a microsecond per request — a large
21
+ // share of total dispatch time — so it only runs once something actually
22
+ // uses the context API. Until then a plain module variable tracks the
23
+ // in-flight request: exact in the synchronous phase of a handler and for
24
+ // non-overlapping requests, which covers the call that flips
25
+ // `contextEnabled`. Every request that starts after the first use gets
26
+ // full ALS isolation. Apps that need guaranteed isolation from the very
27
+ // first request under concurrency can call `enableRequestContext()` at
28
+ // startup.
29
+ let contextEnabled = false
30
+ let syncCurrent: EnhancedRequest | null = null
31
+
32
+ /**
33
+ * Force AsyncLocalStorage-backed request context on for all subsequent
34
+ * requests. Calling `request()`/`getCurrentRequest()` enables it
35
+ * automatically — use this when the very first request must already be
36
+ * fully isolated under concurrent load.
37
+ */
38
+ export function enableRequestContext(): void {
39
+ contextEnabled = true
40
+ }
41
+
20
42
  /**
21
- * Run `fn` inside an AsyncLocalStorage scope where `request()` and
43
+ * Run `fn` inside a request-context scope where `request()` and
22
44
  * `getCurrentRequest()` resolve to `initial`. The Router calls this around
23
45
  * each request automatically.
24
46
  */
25
47
  export function runWithRequest<T>(initial: EnhancedRequest | Request, fn: () => T | Promise<T>): T | Promise<T> {
48
+ if (!contextEnabled) {
49
+ syncCurrent = initial as EnhancedRequest
50
+ const result = fn()
51
+ if (result instanceof Promise) {
52
+ return result.finally(() => {
53
+ syncCurrent = null
54
+ }) as Promise<T>
55
+ }
56
+ syncCurrent = null
57
+ return result
58
+ }
26
59
  return storage.run({ current: initial as EnhancedRequest }, fn)
27
60
  }
28
61
 
@@ -33,16 +66,30 @@ export function runWithRequest<T>(initial: EnhancedRequest | Request, fn: () =>
33
66
  */
34
67
  export function setCurrentRequest(req: EnhancedRequest): void {
35
68
  const store = storage.getStore()
36
- if (store)
69
+ if (store) {
37
70
  store.current = req
71
+ return
72
+ }
73
+ // Pre-enablement fallback: only refresh it while a request is in flight
74
+ if (syncCurrent !== null) {
75
+ syncCurrent = req
76
+ }
38
77
  }
39
78
 
40
79
  /**
41
80
  * Returns the active request, or `undefined` when called outside a request
42
81
  * scope. Prefer `request()` when the request is required.
82
+ *
83
+ * First use switches the router to AsyncLocalStorage-backed context for
84
+ * all subsequent requests; this call itself resolves via a synchronous
85
+ * fallback (see `enableRequestContext` for eager opt-in).
43
86
  */
44
87
  export function getCurrentRequest(): EnhancedRequest | undefined {
45
- return storage.getStore()?.current ?? undefined
88
+ if (!contextEnabled) {
89
+ contextEnabled = true
90
+ return syncCurrent ?? undefined
91
+ }
92
+ return storage.getStore()?.current ?? syncCurrent ?? undefined
46
93
  }
47
94
 
48
95
  /**
@@ -433,8 +433,21 @@ export function enhanceRequestWithMethods(request: EnhancedRequest): EnhancedReq
433
433
  _oldInput?: RequestInput
434
434
  }
435
435
 
436
- // Get all input data
436
+ // Merged input (query + JSON body + form body + route params).
437
+ // Memoized: many of the helpers below call this several times per
438
+ // request, and the merge result only changes when a body is parsed
439
+ // later — so the cache keys off the body object identities.
440
+ let cachedInput: RequestInput | null = null
441
+ let cachedJsonBody: unknown
442
+ let cachedFormBody: unknown
443
+
437
444
  const getAllInput = (): RequestInput => {
445
+ if (cachedInput
446
+ && cachedJsonBody === request.jsonBody
447
+ && cachedFormBody === request.formBody) {
448
+ return cachedInput
449
+ }
450
+
438
451
  const input: RequestInput = {}
439
452
 
440
453
  // Query parameters
@@ -465,6 +478,9 @@ export function enhanceRequestWithMethods(request: EnhancedRequest): EnhancedReq
465
478
  }
466
479
  }
467
480
 
481
+ cachedInput = input
482
+ cachedJsonBody = request.jsonBody
483
+ cachedFormBody = request.formBody
468
484
  return input
469
485
  }
470
486
 
@@ -827,6 +843,9 @@ export function enhanceRequestWithMethods(request: EnhancedRequest): EnhancedReq
827
843
  else {
828
844
  (request as any).jsonBody = data
829
845
  }
846
+ // In-place mutation doesn't change jsonBody's identity, so drop the
847
+ // memoized merge explicitly
848
+ cachedInput = null
830
849
  }
831
850
 
832
851
  /**