@stacksjs/bun-router 0.0.13 → 0.0.14
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.
- package/CHANGELOG.md +770 -0
- package/README.md +584 -261
- package/package.json +43 -47
- package/bin/cli.ts +0 -9
- package/dist/auth.d.ts +0 -134
- package/dist/cache/lru-cache.d.ts +0 -139
- package/dist/cache/middleware-memoization.d.ts +0 -151
- package/dist/cache/route-cache-warmer.d.ts +0 -161
- package/dist/cache/sqlite-cache.d.ts +0 -209
- package/dist/cache/streaming-cache.d.ts +0 -132
- package/dist/chunk-1mvtv6jw.js +0 -13012
- package/dist/cli/colors.d.ts +0 -15
- package/dist/cli/index.d.ts +0 -10
- package/dist/cli/middleware.d.ts +0 -31
- package/dist/cli/openapi.d.ts +0 -17
- package/dist/cli/router.d.ts +0 -15
- package/dist/cli/routes.d.ts +0 -29
- package/dist/cli/utils.d.ts +0 -74
- package/dist/cli.d.ts +0 -1
- package/dist/cli.js +0 -3195
- package/dist/config.d.ts +0 -7
- package/dist/container/container.d.ts +0 -273
- package/dist/container/contextual-binding.d.ts +0 -240
- package/dist/container/decorators.d.ts +0 -141
- package/dist/container/service-provider.d.ts +0 -285
- package/dist/development/hot-reload.d.ts +0 -187
- package/dist/development/index.d.ts +0 -167
- package/dist/development/performance-profiler.d.ts +0 -217
- package/dist/development/route-debugger.d.ts +0 -154
- package/dist/development/route-inspector.d.ts +0 -211
- package/dist/development/typescript-utilities.d.ts +0 -209
- package/dist/docs.d.ts +0 -11
- package/dist/errors/circuit-breaker.d.ts +0 -195
- package/dist/errors/error-handler.d.ts +0 -88
- package/dist/errors/error-reporting.d.ts +0 -144
- package/dist/errors/exceptions.d.ts +0 -184
- package/dist/errors/graceful-degradation.d.ts +0 -154
- package/dist/errors/index.d.ts +0 -9
- package/dist/errors/router-errors.d.ts +0 -110
- package/dist/file-serving/static-files.d.ts +0 -143
- package/dist/index.d.ts +0 -14
- package/dist/index.js +0 -13933
- package/dist/middleware/auth.d.ts +0 -63
- package/dist/middleware/content_security_policy.d.ts +0 -53
- package/dist/middleware/cors.d.ts +0 -5
- package/dist/middleware/csrf.d.ts +0 -7
- package/dist/middleware/ddos_protection.d.ts +0 -39
- package/dist/middleware/file_security.d.ts +0 -26
- package/dist/middleware/file_upload.d.ts +0 -38
- package/dist/middleware/helmet.d.ts +0 -54
- package/dist/middleware/index.d.ts +0 -44
- package/dist/middleware/input_validation.d.ts +0 -45
- package/dist/middleware/json_body.d.ts +0 -4
- package/dist/middleware/performance_alerting.d.ts +0 -87
- package/dist/middleware/performance_dashboard.d.ts +0 -87
- package/dist/middleware/performance_monitor.d.ts +0 -209
- package/dist/middleware/pipeline.d.ts +0 -131
- package/dist/middleware/rate_limit.d.ts +0 -36
- package/dist/middleware/request_id.d.ts +0 -4
- package/dist/middleware/request_signing.d.ts +0 -153
- package/dist/middleware/request_tracer.d.ts +0 -72
- package/dist/middleware/response_cache.d.ts +0 -97
- package/dist/middleware/security.d.ts +0 -69
- package/dist/middleware/security_suite.d.ts +0 -46
- package/dist/middleware/session.d.ts +0 -23
- package/dist/model-binding/index.d.ts +0 -2
- package/dist/model-binding/model-middleware.d.ts +0 -118
- package/dist/model-binding/model-registry.d.ts +0 -164
- package/dist/model-binding.d.ts +0 -185
- package/dist/model-resolver-factory.d.ts +0 -31
- package/dist/observability/correlation.d.ts +0 -187
- package/dist/observability/health-checks.d.ts +0 -186
- package/dist/observability/index.d.ts +0 -60
- package/dist/observability/integration.d.ts +0 -147
- package/dist/observability/metrics.d.ts +0 -184
- package/dist/observability/tracing.d.ts +0 -187
- package/dist/optimization/bun-utilities.d.ts +0 -224
- package/dist/query-builder-integration.d.ts +0 -36
- package/dist/request/context.d.ts +0 -34
- package/dist/request/enhanced-request.d.ts +0 -213
- package/dist/request/macros.d.ts +0 -300
- package/dist/response/macros.d.ts +0 -245
- package/dist/response/response-factory.d.ts +0 -133
- package/dist/router/api-routes.d.ts +0 -5
- package/dist/router/file-based-routing.d.ts +0 -118
- package/dist/router/file-streaming.d.ts +0 -5
- package/dist/router/fluent-router.d.ts +0 -315
- package/dist/router/fluent-routing.d.ts +0 -271
- package/dist/router/group-organization.d.ts +0 -5
- package/dist/router/handler-resolver.d.ts +0 -24
- package/dist/router/http-methods.d.ts +0 -10
- package/dist/router/index.d.ts +0 -114
- package/dist/router/middleware-groups.d.ts +0 -94
- package/dist/router/middleware-integration.d.ts +0 -112
- package/dist/router/middleware.d.ts +0 -5
- package/dist/router/model-binding.d.ts +0 -5
- package/dist/router/optimized-route-matching.d.ts +0 -6
- package/dist/router/route-building.d.ts +0 -5
- package/dist/router/route-compiler.d.ts +0 -153
- package/dist/router/route-matching.d.ts +0 -5
- package/dist/router/route-trie.d.ts +0 -114
- package/dist/router/router.d.ts +0 -294
- package/dist/router/server.d.ts +0 -5
- package/dist/router/validation-integration.d.ts +0 -170
- package/dist/router/view-rendering.d.ts +0 -5
- package/dist/router/websocket.d.ts +0 -5
- package/dist/routing/route-caching.d.ts +0 -129
- package/dist/routing/route-throttling.d.ts +0 -150
- package/dist/routing/subdomain-routing.d.ts +0 -206
- package/dist/session/database-store.d.ts +0 -51
- package/dist/session/file-store.d.ts +0 -22
- package/dist/session/index.d.ts +0 -106
- package/dist/session/memory-store.d.ts +0 -21
- package/dist/session/redis-store.d.ts +0 -31
- package/dist/streaming/index.d.ts +0 -3
- package/dist/streaming/sse-handler.d.ts +0 -138
- package/dist/streaming/stream-handler.d.ts +0 -114
- package/dist/testing/auth-testing.d.ts +0 -156
- package/dist/testing/file-upload-testing.d.ts +0 -190
- package/dist/testing/index.d.ts +0 -10
- package/dist/testing/middleware-testing.d.ts +0 -138
- package/dist/testing/model-binding-testing.d.ts +0 -186
- package/dist/testing/performance-testing.d.ts +0 -235
- package/dist/testing/test-client.d.ts +0 -117
- package/dist/testing/test-request.d.ts +0 -85
- package/dist/testing/test-response.d.ts +0 -90
- package/dist/testing/types.d.ts +0 -207
- package/dist/testing/websocket-testing.d.ts +0 -227
- package/dist/types/controller-types.d.ts +0 -208
- package/dist/types/core.d.ts +0 -543
- package/dist/types/middleware-types.d.ts +0 -227
- package/dist/types/request-response-augmentation.d.ts +0 -261
- package/dist/types/route-inference.d.ts +0 -168
- package/dist/types.d.ts +0 -1782
- package/dist/url.d.ts +0 -56
- package/dist/utils/index.d.ts +0 -1
- package/dist/utils/query-preservation.d.ts +0 -48
- package/dist/utils.d.ts +0 -69
- package/dist/validation/validator.d.ts +0 -140
- package/dist/websocket/clustering.d.ts +0 -185
- package/src/auth.ts +0 -469
- package/src/cache/lru-cache.ts +0 -457
- package/src/cache/middleware-memoization.ts +0 -531
- package/src/cache/route-cache-warmer.ts +0 -486
- package/src/cache/sqlite-cache.ts +0 -783
- package/src/cache/streaming-cache.ts +0 -572
- package/src/cli/colors.ts +0 -29
- package/src/cli/index.ts +0 -287
- package/src/cli/middleware.ts +0 -291
- package/src/cli/openapi.ts +0 -407
- package/src/cli/router.ts +0 -188
- package/src/cli/routes.ts +0 -265
- package/src/cli/utils.ts +0 -531
- package/src/cli.ts +0 -5
- package/src/config.ts +0 -322
- package/src/container/container.ts +0 -740
- package/src/container/contextual-binding.ts +0 -603
- package/src/container/decorators.ts +0 -359
- package/src/container/service-provider.ts +0 -596
- package/src/development/hot-reload.ts +0 -673
- package/src/development/index.ts +0 -499
- package/src/development/performance-profiler.ts +0 -717
- package/src/development/route-debugger.ts +0 -527
- package/src/development/route-inspector.ts +0 -749
- package/src/development/typescript-utilities.ts +0 -682
- package/src/docs.ts +0 -397
- package/src/errors/circuit-breaker.ts +0 -732
- package/src/errors/error-handler.ts +0 -569
- package/src/errors/error-reporting.ts +0 -672
- package/src/errors/exceptions.ts +0 -536
- package/src/errors/graceful-degradation.ts +0 -621
- package/src/errors/index.ts +0 -21
- package/src/errors/router-errors.ts +0 -632
- package/src/file-serving/static-files.ts +0 -581
- package/src/index.ts +0 -14
- package/src/middleware/auth.ts +0 -219
- package/src/middleware/content_security_policy.ts +0 -215
- package/src/middleware/cors.ts +0 -74
- package/src/middleware/csrf.ts +0 -108
- package/src/middleware/ddos_protection.ts +0 -255
- package/src/middleware/file_security.ts +0 -191
- package/src/middleware/file_upload.ts +0 -264
- package/src/middleware/helmet.ts +0 -268
- package/src/middleware/index.ts +0 -117
- package/src/middleware/input_validation.ts +0 -449
- package/src/middleware/json_body.ts +0 -37
- package/src/middleware/performance_alerting.ts +0 -538
- package/src/middleware/performance_dashboard.ts +0 -661
- package/src/middleware/performance_monitor.ts +0 -943
- package/src/middleware/pipeline.ts +0 -489
- package/src/middleware/rate_limit.ts +0 -245
- package/src/middleware/request_id.ts +0 -36
- package/src/middleware/request_signing.ts +0 -636
- package/src/middleware/request_tracer.ts +0 -636
- package/src/middleware/response_cache.ts +0 -743
- package/src/middleware/security.ts +0 -482
- package/src/middleware/security_suite.ts +0 -257
- package/src/middleware/session.ts +0 -91
- package/src/model-binding/index.ts +0 -18
- package/src/model-binding/model-middleware.ts +0 -425
- package/src/model-binding/model-registry.ts +0 -550
- package/src/model-binding.ts +0 -370
- package/src/model-resolver-factory.ts +0 -106
- package/src/observability/correlation.ts +0 -691
- package/src/observability/health-checks.ts +0 -729
- package/src/observability/index.ts +0 -184
- package/src/observability/integration.ts +0 -548
- package/src/observability/metrics.ts +0 -753
- package/src/observability/tracing.ts +0 -638
- package/src/optimization/bun-utilities.ts +0 -778
- package/src/query-builder-integration.ts +0 -137
- package/src/request/context.ts +0 -62
- package/src/request/enhanced-request.ts +0 -857
- package/src/request/macros.ts +0 -688
- package/src/response/macros.ts +0 -612
- package/src/response/response-factory.ts +0 -596
- package/src/router/api-routes.ts +0 -243
- package/src/router/file-based-routing.ts +0 -670
- package/src/router/file-streaming.ts +0 -372
- package/src/router/fluent-router.ts +0 -927
- package/src/router/fluent-routing.ts +0 -797
- package/src/router/group-organization.ts +0 -213
- package/src/router/handler-resolver.ts +0 -291
- package/src/router/http-methods.ts +0 -377
- package/src/router/index.ts +0 -187
- package/src/router/middleware-groups.ts +0 -222
- package/src/router/middleware-integration.ts +0 -399
- package/src/router/middleware.ts +0 -231
- package/src/router/model-binding.ts +0 -215
- package/src/router/optimized-route-matching.ts +0 -253
- package/src/router/route-building.ts +0 -212
- package/src/router/route-compiler.ts +0 -702
- package/src/router/route-matching.ts +0 -349
- package/src/router/route-trie.ts +0 -464
- package/src/router/router.ts +0 -1581
- package/src/router/server.ts +0 -462
- package/src/router/validation-integration.ts +0 -443
- package/src/router/view-rendering.ts +0 -233
- package/src/router/websocket.ts +0 -100
- package/src/routing/route-caching.ts +0 -402
- package/src/routing/route-throttling.ts +0 -469
- package/src/routing/subdomain-routing.ts +0 -492
- package/src/session/database-store.ts +0 -109
- package/src/session/file-store.ts +0 -148
- package/src/session/index.ts +0 -244
- package/src/session/memory-store.ts +0 -88
- package/src/session/redis-store.ts +0 -93
- package/src/streaming/index.ts +0 -17
- package/src/streaming/sse-handler.ts +0 -482
- package/src/streaming/stream-handler.ts +0 -552
- package/src/testing/auth-testing.ts +0 -446
- package/src/testing/file-upload-testing.ts +0 -543
- package/src/testing/index.ts +0 -10
- package/src/testing/middleware-testing.ts +0 -322
- package/src/testing/model-binding-testing.ts +0 -645
- package/src/testing/performance-testing.ts +0 -738
- package/src/testing/test-client.ts +0 -310
- package/src/testing/test-request.ts +0 -315
- package/src/testing/test-response.ts +0 -332
- package/src/testing/types.ts +0 -224
- package/src/testing/websocket-testing.ts +0 -590
- package/src/types/controller-types.ts +0 -385
- package/src/types/core.ts +0 -683
- package/src/types/middleware-types.ts +0 -419
- package/src/types/request-response-augmentation.ts +0 -489
- package/src/types/route-inference.ts +0 -357
- package/src/types.ts +0 -2055
- package/src/url.ts +0 -126
- package/src/utils/index.ts +0 -1
- package/src/utils/query-preservation.ts +0 -201
- package/src/utils.ts +0 -327
- package/src/validation/validator.ts +0 -685
- package/src/websocket/clustering.ts +0 -762
|
@@ -1,552 +0,0 @@
|
|
|
1
|
-
import type { BunFile } from 'bun'
|
|
2
|
-
import type {
|
|
3
|
-
BufferedStreamConfig,
|
|
4
|
-
ContentType,
|
|
5
|
-
DirectStreamConfig,
|
|
6
|
-
EnhancedRequest,
|
|
7
|
-
ResponseStatus,
|
|
8
|
-
StreamingCompression,
|
|
9
|
-
StreamingFormat,
|
|
10
|
-
} from '../types'
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Base streaming configuration
|
|
14
|
-
*/
|
|
15
|
-
export interface BaseStreamConfig {
|
|
16
|
-
contentType?: ContentType
|
|
17
|
-
headers?: Record<string, string>
|
|
18
|
-
status?: ResponseStatus
|
|
19
|
-
bufferSize?: number
|
|
20
|
-
enableCompression?: boolean
|
|
21
|
-
compressionType?: StreamingCompression
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Stream response wrapper
|
|
26
|
-
*/
|
|
27
|
-
export class StreamResponse {
|
|
28
|
-
private stream: ReadableStream
|
|
29
|
-
private headers: Headers
|
|
30
|
-
private status: ResponseStatus
|
|
31
|
-
|
|
32
|
-
constructor(
|
|
33
|
-
stream: ReadableStream,
|
|
34
|
-
options: {
|
|
35
|
-
headers?: Headers | Record<string, string>
|
|
36
|
-
status?: ResponseStatus
|
|
37
|
-
contentType?: ContentType
|
|
38
|
-
} = {},
|
|
39
|
-
) {
|
|
40
|
-
this.stream = stream
|
|
41
|
-
this.status = options.status || 200
|
|
42
|
-
this.headers = new Headers(options.headers)
|
|
43
|
-
|
|
44
|
-
if (options.contentType) {
|
|
45
|
-
this.headers.set('Content-Type', options.contentType)
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
toResponse(): Response {
|
|
50
|
-
return new Response(this.stream, {
|
|
51
|
-
status: this.status,
|
|
52
|
-
headers: this.headers,
|
|
53
|
-
})
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
addHeader(name: string, value: string): this {
|
|
57
|
-
this.headers.set(name, value)
|
|
58
|
-
return this
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
setContentType(contentType: ContentType): this {
|
|
62
|
-
this.headers.set('Content-Type', contentType)
|
|
63
|
-
return this
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
setStatus(status: ResponseStatus): this {
|
|
67
|
-
this.status = status
|
|
68
|
-
return this
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Generic streaming handler
|
|
74
|
-
*/
|
|
75
|
-
export class StreamHandler {
|
|
76
|
-
/**
|
|
77
|
-
* Create a direct stream response
|
|
78
|
-
*/
|
|
79
|
-
static direct<T>(
|
|
80
|
-
data: T[] | AsyncIterable<T>,
|
|
81
|
-
config: DirectStreamConfig = {},
|
|
82
|
-
): StreamResponse {
|
|
83
|
-
const {
|
|
84
|
-
format = 'json',
|
|
85
|
-
delimiter = '\n',
|
|
86
|
-
contentType,
|
|
87
|
-
headers = {},
|
|
88
|
-
status = 200,
|
|
89
|
-
enableCompression: _enableCompression = false,
|
|
90
|
-
} = config
|
|
91
|
-
|
|
92
|
-
const stream = new ReadableStream({
|
|
93
|
-
async start(controller) {
|
|
94
|
-
try {
|
|
95
|
-
const encoder = new TextEncoder()
|
|
96
|
-
let isFirst = true
|
|
97
|
-
|
|
98
|
-
for await (const item of Array.isArray(data) ? data : data) {
|
|
99
|
-
if (!isFirst && delimiter) {
|
|
100
|
-
controller.enqueue(encoder.encode(delimiter))
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const formatted = StreamHandler.formatData(item, format)
|
|
104
|
-
controller.enqueue(encoder.encode(formatted))
|
|
105
|
-
isFirst = false
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
controller.close()
|
|
109
|
-
}
|
|
110
|
-
catch (error) {
|
|
111
|
-
controller.error(error)
|
|
112
|
-
}
|
|
113
|
-
},
|
|
114
|
-
})
|
|
115
|
-
|
|
116
|
-
const responseHeaders = new Headers(headers)
|
|
117
|
-
if (contentType) {
|
|
118
|
-
responseHeaders.set('Content-Type', contentType)
|
|
119
|
-
}
|
|
120
|
-
else {
|
|
121
|
-
responseHeaders.set('Content-Type', StreamHandler.getContentTypeForFormat(format))
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
return new StreamResponse(stream, {
|
|
125
|
-
headers: responseHeaders,
|
|
126
|
-
status,
|
|
127
|
-
})
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Create a buffered stream response
|
|
132
|
-
*/
|
|
133
|
-
static buffered<T>(
|
|
134
|
-
data: T[] | AsyncIterable<T>,
|
|
135
|
-
config: BufferedStreamConfig = {},
|
|
136
|
-
): StreamResponse {
|
|
137
|
-
const {
|
|
138
|
-
format = 'json',
|
|
139
|
-
bufferSize = 1024,
|
|
140
|
-
flushInterval = 100,
|
|
141
|
-
contentType,
|
|
142
|
-
headers = {},
|
|
143
|
-
status = 200,
|
|
144
|
-
} = config
|
|
145
|
-
|
|
146
|
-
const stream = new ReadableStream({
|
|
147
|
-
async start(controller) {
|
|
148
|
-
try {
|
|
149
|
-
const encoder = new TextEncoder()
|
|
150
|
-
let buffer: string[] = []
|
|
151
|
-
let bufferLength = 0
|
|
152
|
-
|
|
153
|
-
const flush = () => {
|
|
154
|
-
if (buffer.length > 0) {
|
|
155
|
-
const chunk = buffer.join('')
|
|
156
|
-
controller.enqueue(encoder.encode(chunk))
|
|
157
|
-
buffer = []
|
|
158
|
-
bufferLength = 0
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// Set up flush interval
|
|
163
|
-
const flushTimer = setInterval(flush, flushInterval)
|
|
164
|
-
|
|
165
|
-
try {
|
|
166
|
-
for await (const item of Array.isArray(data) ? data : data) {
|
|
167
|
-
const formatted = StreamHandler.formatData(item, format)
|
|
168
|
-
buffer.push(formatted)
|
|
169
|
-
bufferLength += formatted.length
|
|
170
|
-
|
|
171
|
-
if (bufferLength >= bufferSize) {
|
|
172
|
-
flush()
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// Final flush
|
|
177
|
-
flush()
|
|
178
|
-
}
|
|
179
|
-
finally {
|
|
180
|
-
clearInterval(flushTimer)
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
controller.close()
|
|
184
|
-
}
|
|
185
|
-
catch (error) {
|
|
186
|
-
controller.error(error)
|
|
187
|
-
}
|
|
188
|
-
},
|
|
189
|
-
})
|
|
190
|
-
|
|
191
|
-
const responseHeaders = new Headers(headers)
|
|
192
|
-
if (contentType) {
|
|
193
|
-
responseHeaders.set('Content-Type', contentType)
|
|
194
|
-
}
|
|
195
|
-
else {
|
|
196
|
-
responseHeaders.set('Content-Type', StreamHandler.getContentTypeForFormat(format))
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
return new StreamResponse(stream, {
|
|
200
|
-
headers: responseHeaders,
|
|
201
|
-
status,
|
|
202
|
-
})
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Create a JSON stream response
|
|
207
|
-
*/
|
|
208
|
-
static json<T>(
|
|
209
|
-
data: T[] | AsyncIterable<T>,
|
|
210
|
-
config: BaseStreamConfig = {},
|
|
211
|
-
): StreamResponse {
|
|
212
|
-
return StreamHandler.direct(data, {
|
|
213
|
-
...config,
|
|
214
|
-
format: 'json',
|
|
215
|
-
contentType: config.contentType || 'application/json',
|
|
216
|
-
})
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
/**
|
|
220
|
-
* Create a text stream response
|
|
221
|
-
*/
|
|
222
|
-
static text(
|
|
223
|
-
data: string[] | AsyncIterable<string>,
|
|
224
|
-
config: BaseStreamConfig = {},
|
|
225
|
-
): StreamResponse {
|
|
226
|
-
return StreamHandler.direct(data, {
|
|
227
|
-
...config,
|
|
228
|
-
format: 'text',
|
|
229
|
-
contentType: config.contentType || 'text/plain',
|
|
230
|
-
})
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
/**
|
|
234
|
-
* Create a CSV stream response
|
|
235
|
-
*/
|
|
236
|
-
static csv<T extends Record<string, any>>(
|
|
237
|
-
data: T[] | AsyncIterable<T>,
|
|
238
|
-
config: BaseStreamConfig & { csvHeaders?: string[] } = {},
|
|
239
|
-
): StreamResponse {
|
|
240
|
-
const { csvHeaders, ...streamConfig } = config
|
|
241
|
-
|
|
242
|
-
const stream = new ReadableStream({
|
|
243
|
-
async start(controller) {
|
|
244
|
-
try {
|
|
245
|
-
const encoder = new TextEncoder()
|
|
246
|
-
const _isFirst = true
|
|
247
|
-
|
|
248
|
-
// Write CSV headers if provided
|
|
249
|
-
if (csvHeaders && csvHeaders.length > 0) {
|
|
250
|
-
controller.enqueue(encoder.encode(`${csvHeaders.join(',')}\n`))
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
for await (const item of Array.isArray(data) ? data : data) {
|
|
254
|
-
const csvRow = StreamHandler.objectToCsvRow(item, csvHeaders)
|
|
255
|
-
controller.enqueue(encoder.encode(`${csvRow}\n`))
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
controller.close()
|
|
259
|
-
}
|
|
260
|
-
catch (error) {
|
|
261
|
-
controller.error(error)
|
|
262
|
-
}
|
|
263
|
-
},
|
|
264
|
-
})
|
|
265
|
-
|
|
266
|
-
return new StreamResponse(stream, {
|
|
267
|
-
headers: streamConfig.headers,
|
|
268
|
-
status: streamConfig.status,
|
|
269
|
-
contentType: streamConfig.contentType || 'text/csv',
|
|
270
|
-
})
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
/**
|
|
274
|
-
* Format data according to the specified format
|
|
275
|
-
*/
|
|
276
|
-
private static formatData<T>(data: T, format: StreamingFormat): string {
|
|
277
|
-
switch (format) {
|
|
278
|
-
case 'json':
|
|
279
|
-
return JSON.stringify(data)
|
|
280
|
-
case 'text':
|
|
281
|
-
return String(data)
|
|
282
|
-
case 'csv':
|
|
283
|
-
return typeof data === 'object' ? StreamHandler.objectToCsvRow(data as Record<string, any>) : String(data)
|
|
284
|
-
case 'ndjson':
|
|
285
|
-
return JSON.stringify(data)
|
|
286
|
-
default:
|
|
287
|
-
return JSON.stringify(data)
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
/**
|
|
292
|
-
* Convert object to CSV row
|
|
293
|
-
*/
|
|
294
|
-
private static objectToCsvRow(obj: Record<string, any>, headers?: string[]): string {
|
|
295
|
-
const keys = headers || Object.keys(obj)
|
|
296
|
-
// eslint-disable-next-line pickier/no-unused-vars
|
|
297
|
-
return keys.map((key) => {
|
|
298
|
-
const value = obj[key]
|
|
299
|
-
const stringValue = value === null || value === undefined ? '' : String(value)
|
|
300
|
-
// Escape quotes and wrap in quotes if contains comma, quote, or newline
|
|
301
|
-
if (stringValue.includes(',') || stringValue.includes('"') || stringValue.includes('\n')) {
|
|
302
|
-
return `"${stringValue.replace(/"/g, '""')}"`
|
|
303
|
-
}
|
|
304
|
-
return stringValue
|
|
305
|
-
}).join(',')
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
/**
|
|
309
|
-
* Get appropriate content type for format
|
|
310
|
-
*/
|
|
311
|
-
private static getContentTypeForFormat(format: StreamingFormat): ContentType {
|
|
312
|
-
switch (format) {
|
|
313
|
-
case 'json':
|
|
314
|
-
case 'ndjson':
|
|
315
|
-
return 'application/json'
|
|
316
|
-
case 'text':
|
|
317
|
-
return 'text/plain'
|
|
318
|
-
case 'csv':
|
|
319
|
-
return 'text/csv'
|
|
320
|
-
default:
|
|
321
|
-
return 'application/json'
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
/**
|
|
327
|
-
* Stream utilities
|
|
328
|
-
*/
|
|
329
|
-
export const StreamUtils = {
|
|
330
|
-
/**
|
|
331
|
-
* Create a transform stream
|
|
332
|
-
*/
|
|
333
|
-
transform<T, U>(
|
|
334
|
-
transformFn: (chunk: T) => U | Promise<U>,
|
|
335
|
-
): TransformStream<T, U> {
|
|
336
|
-
return new TransformStream({
|
|
337
|
-
async transform(chunk, controller) {
|
|
338
|
-
try {
|
|
339
|
-
const transformed = await transformFn(chunk)
|
|
340
|
-
controller.enqueue(transformed)
|
|
341
|
-
}
|
|
342
|
-
catch (error) {
|
|
343
|
-
controller.error(error)
|
|
344
|
-
}
|
|
345
|
-
},
|
|
346
|
-
})
|
|
347
|
-
},
|
|
348
|
-
|
|
349
|
-
/**
|
|
350
|
-
* Create a filter stream
|
|
351
|
-
*/
|
|
352
|
-
filter<T>(
|
|
353
|
-
filterFn: (chunk: T) => boolean | Promise<boolean>,
|
|
354
|
-
): TransformStream<T, T> {
|
|
355
|
-
return new TransformStream({
|
|
356
|
-
async transform(chunk, controller) {
|
|
357
|
-
try {
|
|
358
|
-
const shouldInclude = await filterFn(chunk)
|
|
359
|
-
if (shouldInclude) {
|
|
360
|
-
controller.enqueue(chunk)
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
catch (error) {
|
|
364
|
-
controller.error(error)
|
|
365
|
-
}
|
|
366
|
-
},
|
|
367
|
-
})
|
|
368
|
-
},
|
|
369
|
-
|
|
370
|
-
/**
|
|
371
|
-
* Create a batch stream
|
|
372
|
-
*/
|
|
373
|
-
batch<T>(batchSize: number): TransformStream<T, T[]> {
|
|
374
|
-
let batch: T[] = []
|
|
375
|
-
|
|
376
|
-
return new TransformStream({
|
|
377
|
-
transform(chunk, controller) {
|
|
378
|
-
batch.push(chunk)
|
|
379
|
-
if (batch.length >= batchSize) {
|
|
380
|
-
controller.enqueue([...batch])
|
|
381
|
-
batch = []
|
|
382
|
-
}
|
|
383
|
-
},
|
|
384
|
-
flush(controller) {
|
|
385
|
-
if (batch.length > 0) {
|
|
386
|
-
controller.enqueue(batch)
|
|
387
|
-
}
|
|
388
|
-
},
|
|
389
|
-
})
|
|
390
|
-
},
|
|
391
|
-
|
|
392
|
-
/**
|
|
393
|
-
* Create a delay stream
|
|
394
|
-
*/
|
|
395
|
-
delay<T>(ms: number): TransformStream<T, T> {
|
|
396
|
-
return new TransformStream({
|
|
397
|
-
async transform(chunk, controller) {
|
|
398
|
-
await new Promise(resolve => setTimeout(resolve, ms))
|
|
399
|
-
controller.enqueue(chunk)
|
|
400
|
-
},
|
|
401
|
-
})
|
|
402
|
-
},
|
|
403
|
-
|
|
404
|
-
/**
|
|
405
|
-
* Create a rate limit stream
|
|
406
|
-
*/
|
|
407
|
-
rateLimit<T>(itemsPerSecond: number): TransformStream<T, T> {
|
|
408
|
-
const interval = 1000 / itemsPerSecond
|
|
409
|
-
let lastEmit = 0
|
|
410
|
-
|
|
411
|
-
return new TransformStream({
|
|
412
|
-
async transform(chunk, controller) {
|
|
413
|
-
const now = Date.now()
|
|
414
|
-
const timeSinceLastEmit = now - lastEmit
|
|
415
|
-
|
|
416
|
-
if (timeSinceLastEmit < interval) {
|
|
417
|
-
await new Promise(resolve => setTimeout(resolve, interval - timeSinceLastEmit))
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
lastEmit = Date.now()
|
|
421
|
-
controller.enqueue(chunk)
|
|
422
|
-
},
|
|
423
|
-
})
|
|
424
|
-
},
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
/**
|
|
428
|
-
* File streaming utilities
|
|
429
|
-
*/
|
|
430
|
-
export class FileStreamHandler {
|
|
431
|
-
/**
|
|
432
|
-
* Stream a file with range support
|
|
433
|
-
*/
|
|
434
|
-
static async streamFile(
|
|
435
|
-
filePath: string,
|
|
436
|
-
request: EnhancedRequest,
|
|
437
|
-
options: {
|
|
438
|
-
contentType?: ContentType
|
|
439
|
-
enableRanges?: boolean
|
|
440
|
-
chunkSize?: number
|
|
441
|
-
} = {},
|
|
442
|
-
): Promise<Response> {
|
|
443
|
-
const { contentType, enableRanges = true, chunkSize: _chunkSize = 64 * 1024 } = options
|
|
444
|
-
|
|
445
|
-
try {
|
|
446
|
-
const file = Bun.file(filePath)
|
|
447
|
-
const fileSize = file.size
|
|
448
|
-
const range = enableRanges ? request.headers.get('range') : null
|
|
449
|
-
|
|
450
|
-
if (range && enableRanges) {
|
|
451
|
-
return FileStreamHandler.handleRangeRequest(file, range, contentType)
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
const headers = new Headers({
|
|
455
|
-
'Content-Length': fileSize.toString(),
|
|
456
|
-
'Accept-Ranges': enableRanges ? 'bytes' : 'none',
|
|
457
|
-
})
|
|
458
|
-
|
|
459
|
-
if (contentType) {
|
|
460
|
-
headers.set('Content-Type', contentType)
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
return new Response(file.stream(), {
|
|
464
|
-
status: 200,
|
|
465
|
-
headers,
|
|
466
|
-
})
|
|
467
|
-
}
|
|
468
|
-
catch (error) {
|
|
469
|
-
console.error('Error streaming file:', error)
|
|
470
|
-
return new Response('File not found', { status: 404 })
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
/**
|
|
475
|
-
* Handle range requests for file streaming
|
|
476
|
-
*/
|
|
477
|
-
private static async handleRangeRequest(
|
|
478
|
-
file: BunFile,
|
|
479
|
-
rangeHeader: string,
|
|
480
|
-
contentType?: ContentType,
|
|
481
|
-
): Promise<Response> {
|
|
482
|
-
const ranges = FileStreamHandler.parseRangeHeader(rangeHeader, file.size)
|
|
483
|
-
|
|
484
|
-
if (!ranges || ranges.length === 0) {
|
|
485
|
-
return new Response('Invalid range', { status: 416 })
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
const [start, end] = ranges[0]
|
|
489
|
-
const contentLength = end - start + 1
|
|
490
|
-
|
|
491
|
-
const headers = new Headers({
|
|
492
|
-
'Content-Range': `bytes ${start}-${end}/${file.size}`,
|
|
493
|
-
'Content-Length': contentLength.toString(),
|
|
494
|
-
'Accept-Ranges': 'bytes',
|
|
495
|
-
})
|
|
496
|
-
|
|
497
|
-
if (contentType) {
|
|
498
|
-
headers.set('Content-Type', contentType)
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
const slice = file.slice(start, end + 1)
|
|
502
|
-
|
|
503
|
-
return new Response(slice.stream(), {
|
|
504
|
-
status: 206,
|
|
505
|
-
headers,
|
|
506
|
-
})
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
/**
|
|
510
|
-
* Parse Range header
|
|
511
|
-
*/
|
|
512
|
-
private static parseRangeHeader(rangeHeader: string, fileSize: number): [number, number][] | null {
|
|
513
|
-
const matches = rangeHeader.match(/^bytes=(.+)$/)
|
|
514
|
-
if (!matches)
|
|
515
|
-
return null
|
|
516
|
-
|
|
517
|
-
const ranges: [number, number][] = []
|
|
518
|
-
const rangeSpecs = matches[1].split(',')
|
|
519
|
-
|
|
520
|
-
for (const rangeSpec of rangeSpecs) {
|
|
521
|
-
const [startStr, endStr] = rangeSpec.trim().split('-')
|
|
522
|
-
|
|
523
|
-
let start: number
|
|
524
|
-
let end: number
|
|
525
|
-
|
|
526
|
-
if (startStr === '') {
|
|
527
|
-
// Suffix range: -500 (last 500 bytes)
|
|
528
|
-
start = Math.max(0, fileSize - Number.parseInt(endStr, 10))
|
|
529
|
-
end = fileSize - 1
|
|
530
|
-
}
|
|
531
|
-
else if (endStr === '') {
|
|
532
|
-
// Prefix range: 500- (from byte 500 to end)
|
|
533
|
-
start = Number.parseInt(startStr, 10)
|
|
534
|
-
end = fileSize - 1
|
|
535
|
-
}
|
|
536
|
-
else {
|
|
537
|
-
// Full range: 500-1000
|
|
538
|
-
start = Number.parseInt(startStr, 10)
|
|
539
|
-
end = Number.parseInt(endStr, 10)
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
// Validate range
|
|
543
|
-
if (start < 0 || end >= fileSize || start > end) {
|
|
544
|
-
return null
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
ranges.push([start, end])
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
return ranges
|
|
551
|
-
}
|
|
552
|
-
}
|