api-ape 3.0.1 → 4.1.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.
Files changed (186) hide show
  1. package/README.md +58 -570
  2. package/client/README.md +73 -14
  3. package/client/auth/crypto/aead.js +214 -0
  4. package/client/auth/crypto/constants.js +32 -0
  5. package/client/auth/crypto/encoding.js +104 -0
  6. package/client/auth/crypto/files.md +27 -0
  7. package/client/auth/crypto/kdf.js +217 -0
  8. package/client/auth/crypto-utils.js +118 -0
  9. package/client/auth/files.md +52 -0
  10. package/client/auth/key-recovery.js +288 -0
  11. package/client/auth/recovery/constants.js +37 -0
  12. package/client/auth/recovery/files.md +23 -0
  13. package/client/auth/recovery/key-derivation.js +61 -0
  14. package/client/auth/recovery/sss-browser.js +189 -0
  15. package/client/auth/share-storage.js +205 -0
  16. package/client/auth/storage/constants.js +18 -0
  17. package/client/auth/storage/db.js +132 -0
  18. package/client/auth/storage/files.md +27 -0
  19. package/client/auth/storage/keys.js +173 -0
  20. package/client/auth/storage/shares.js +200 -0
  21. package/client/browser.js +190 -23
  22. package/client/connectSocket.js +418 -988
  23. package/client/connection/README.md +23 -0
  24. package/client/connection/fileDownload.js +256 -0
  25. package/client/connection/fileHandling.js +450 -0
  26. package/client/connection/fileUtils.js +346 -0
  27. package/client/connection/files.md +71 -0
  28. package/client/connection/messageHandler.js +105 -0
  29. package/client/connection/network.js +350 -0
  30. package/client/connection/proxy.js +233 -0
  31. package/client/connection/sender.js +333 -0
  32. package/client/connection/state.js +321 -0
  33. package/client/connection/subscriptions.js +151 -0
  34. package/client/files.md +53 -0
  35. package/client/index.js +298 -142
  36. package/client/transports/README.md +50 -0
  37. package/client/transports/files.md +41 -0
  38. package/client/transports/streamParser.js +195 -0
  39. package/client/transports/streaming.js +555 -202
  40. package/dist/ape.js +6 -1
  41. package/dist/ape.js.map +4 -4
  42. package/index.d.ts +38 -16
  43. package/package.json +32 -7
  44. package/server/README.md +287 -53
  45. package/server/adapters/README.md +28 -19
  46. package/server/adapters/files.md +68 -0
  47. package/server/adapters/firebase.js +543 -160
  48. package/server/adapters/index.js +362 -112
  49. package/server/adapters/mongo.js +530 -140
  50. package/server/adapters/postgres.js +534 -155
  51. package/server/adapters/redis.js +508 -143
  52. package/server/adapters/supabase.js +555 -186
  53. package/server/client/README.md +43 -0
  54. package/server/client/connection.js +586 -0
  55. package/server/client/files.md +40 -0
  56. package/server/client/index.js +342 -0
  57. package/server/files.md +54 -0
  58. package/server/index.js +332 -27
  59. package/server/lib/README.md +26 -0
  60. package/server/lib/broadcast/clients.js +219 -0
  61. package/server/lib/broadcast/files.md +58 -0
  62. package/server/lib/broadcast/index.js +57 -0
  63. package/server/lib/broadcast/publishProxy.js +110 -0
  64. package/server/lib/broadcast/pubsub.js +137 -0
  65. package/server/lib/broadcast/sendProxy.js +103 -0
  66. package/server/lib/bun.js +315 -99
  67. package/server/lib/fileTransfer/README.md +63 -0
  68. package/server/lib/fileTransfer/files.md +30 -0
  69. package/server/lib/fileTransfer/streaming.js +435 -0
  70. package/server/lib/fileTransfer.js +710 -326
  71. package/server/lib/files.md +111 -0
  72. package/server/lib/httpUtils.js +283 -0
  73. package/server/lib/loader.js +208 -7
  74. package/server/lib/longPolling/README.md +63 -0
  75. package/server/lib/longPolling/files.md +44 -0
  76. package/server/lib/longPolling/getHandler.js +365 -0
  77. package/server/lib/longPolling/postHandler.js +327 -0
  78. package/server/lib/longPolling.js +174 -221
  79. package/server/lib/main.js +369 -532
  80. package/server/lib/runtimes/README.md +42 -0
  81. package/server/lib/runtimes/bun.js +586 -0
  82. package/server/lib/runtimes/files.md +56 -0
  83. package/server/lib/runtimes/node.js +511 -0
  84. package/server/lib/wiring.js +539 -98
  85. package/server/lib/ws/README.md +35 -0
  86. package/server/lib/ws/adapters/README.md +54 -0
  87. package/server/lib/ws/adapters/bun.js +538 -170
  88. package/server/lib/ws/adapters/deno.js +623 -149
  89. package/server/lib/ws/adapters/files.md +42 -0
  90. package/server/lib/ws/files.md +74 -0
  91. package/server/lib/ws/frames.js +532 -154
  92. package/server/lib/ws/index.js +207 -10
  93. package/server/lib/ws/server.js +385 -92
  94. package/server/lib/ws/socket.js +549 -181
  95. package/server/lib/wsProvider.js +363 -89
  96. package/server/plugins/binary.js +282 -0
  97. package/server/security/README.md +92 -0
  98. package/server/security/auth/README.md +319 -0
  99. package/server/security/auth/adapters/files.md +95 -0
  100. package/server/security/auth/adapters/ldap/constants.js +37 -0
  101. package/server/security/auth/adapters/ldap/files.md +19 -0
  102. package/server/security/auth/adapters/ldap/helpers.js +111 -0
  103. package/server/security/auth/adapters/ldap.js +353 -0
  104. package/server/security/auth/adapters/oauth2/constants.js +41 -0
  105. package/server/security/auth/adapters/oauth2/files.md +19 -0
  106. package/server/security/auth/adapters/oauth2/helpers.js +123 -0
  107. package/server/security/auth/adapters/oauth2.js +273 -0
  108. package/server/security/auth/adapters/opaque-handlers.js +314 -0
  109. package/server/security/auth/adapters/opaque.js +205 -0
  110. package/server/security/auth/adapters/saml/constants.js +52 -0
  111. package/server/security/auth/adapters/saml/files.md +19 -0
  112. package/server/security/auth/adapters/saml/helpers.js +74 -0
  113. package/server/security/auth/adapters/saml.js +173 -0
  114. package/server/security/auth/adapters/totp.js +703 -0
  115. package/server/security/auth/adapters/webauthn.js +625 -0
  116. package/server/security/auth/files.md +61 -0
  117. package/server/security/auth/framework/constants.js +27 -0
  118. package/server/security/auth/framework/files.md +23 -0
  119. package/server/security/auth/framework/handlers.js +272 -0
  120. package/server/security/auth/framework/socket-auth.js +177 -0
  121. package/server/security/auth/handlers/auth-messages.js +143 -0
  122. package/server/security/auth/handlers/files.md +28 -0
  123. package/server/security/auth/index.js +290 -0
  124. package/server/security/auth/mfa/crypto/aead.js +148 -0
  125. package/server/security/auth/mfa/crypto/constants.js +35 -0
  126. package/server/security/auth/mfa/crypto/files.md +27 -0
  127. package/server/security/auth/mfa/crypto/kdf.js +120 -0
  128. package/server/security/auth/mfa/crypto/utils.js +68 -0
  129. package/server/security/auth/mfa/crypto-utils.js +80 -0
  130. package/server/security/auth/mfa/files.md +77 -0
  131. package/server/security/auth/mfa/ledger/constants.js +75 -0
  132. package/server/security/auth/mfa/ledger/errors.js +73 -0
  133. package/server/security/auth/mfa/ledger/files.md +23 -0
  134. package/server/security/auth/mfa/ledger/share-record.js +32 -0
  135. package/server/security/auth/mfa/ledger.js +255 -0
  136. package/server/security/auth/mfa/recovery/constants.js +67 -0
  137. package/server/security/auth/mfa/recovery/files.md +19 -0
  138. package/server/security/auth/mfa/recovery/handlers.js +216 -0
  139. package/server/security/auth/mfa/recovery.js +191 -0
  140. package/server/security/auth/mfa/sss/constants.js +21 -0
  141. package/server/security/auth/mfa/sss/files.md +23 -0
  142. package/server/security/auth/mfa/sss/gf256.js +103 -0
  143. package/server/security/auth/mfa/sss/serialization.js +82 -0
  144. package/server/security/auth/mfa/sss.js +161 -0
  145. package/server/security/auth/mfa/two-of-three/constants.js +58 -0
  146. package/server/security/auth/mfa/two-of-three/files.md +23 -0
  147. package/server/security/auth/mfa/two-of-three/handlers.js +241 -0
  148. package/server/security/auth/mfa/two-of-three/helpers.js +71 -0
  149. package/server/security/auth/mfa/two-of-three.js +136 -0
  150. package/server/security/auth/nonce-manager.js +89 -0
  151. package/server/security/auth/state-machine-mfa.js +269 -0
  152. package/server/security/auth/state-machine.js +257 -0
  153. package/server/security/extractRootDomain.js +144 -16
  154. package/server/security/files.md +51 -0
  155. package/server/security/origin.js +197 -15
  156. package/server/security/reply.js +274 -16
  157. package/server/socket/README.md +119 -0
  158. package/server/socket/authMiddleware.js +299 -0
  159. package/server/socket/files.md +86 -0
  160. package/server/socket/open.js +154 -8
  161. package/server/socket/pluginHooks.js +334 -0
  162. package/server/socket/receive.js +184 -225
  163. package/server/socket/receiveContext.js +117 -0
  164. package/server/socket/send.js +416 -78
  165. package/server/socket/tagUtils.js +402 -0
  166. package/server/utils/README.md +19 -0
  167. package/server/utils/deepRequire.js +255 -30
  168. package/server/utils/files.md +57 -0
  169. package/server/utils/genId.js +182 -20
  170. package/server/utils/parseUserAgent.js +313 -251
  171. package/server/utils/userAgent/README.md +65 -0
  172. package/server/utils/userAgent/files.md +46 -0
  173. package/server/utils/userAgent/patterns.js +545 -0
  174. package/utils/README.md +21 -0
  175. package/utils/files.md +66 -0
  176. package/utils/jss/README.md +21 -0
  177. package/utils/jss/decode.js +471 -0
  178. package/utils/jss/encode.js +312 -0
  179. package/utils/jss/files.md +68 -0
  180. package/utils/jss/plugins.js +210 -0
  181. package/utils/jss.js +219 -273
  182. package/utils/messageHash.js +238 -35
  183. package/dist/api-ape.min.js +0 -2
  184. package/dist/api-ape.min.js.map +0 -7
  185. package/server/client.js +0 -308
  186. package/server/lib/broadcast.js +0 -146
@@ -0,0 +1,435 @@
1
+ /**
2
+ * @fileoverview Streaming File Manager - Client-to-Client File Transfers
3
+ *
4
+ * This module provides a specialized manager for handling streaming file transfers
5
+ * between clients. Unlike traditional uploads where the server receives the complete
6
+ * file, streaming transfers allow data to flow through the server in chunks,
7
+ * enabling real-time file sharing between clients.
8
+ *
9
+ * Use Cases:
10
+ * - Live file sharing in collaborative applications
11
+ * - Large file transfers without server-side storage
12
+ * - Real-time media streaming between peers
13
+ * - Progressive file downloads while upload is in progress
14
+ *
15
+ * How Streaming Transfers Work:
16
+ * 1. Uploader registers a streaming file with a unique ID
17
+ * 2. Uploader sends chunks via `appendChunk()` as data becomes available
18
+ * 3. Downloader(s) can read partial data via `get()` with offset
19
+ * 4. Uploader calls `complete()` when transfer is finished
20
+ * 5. File is automatically cleaned up after timeout
21
+ *
22
+ * Memory Management:
23
+ * - Chunks are stored in memory as Buffer arrays
24
+ * - Automatic cleanup via configurable timeouts
25
+ * - `destroy()` method for immediate cleanup
26
+ *
27
+ * @module server/lib/fileTransfer/streaming
28
+ * @see {@link module:server/lib/fileTransfer} - Main file transfer module
29
+ *
30
+ * @example
31
+ * // Create a streaming file manager
32
+ * const { StreamingFileManager } = require('./streaming')
33
+ *
34
+ * const manager = new StreamingFileManager(
35
+ * 30000, // 30 second start timeout
36
+ * 60000 // 60 second complete timeout
37
+ * )
38
+ *
39
+ * // Register a new streaming file
40
+ * const fileId = manager.register('file-abc', 'uploader-client-id')
41
+ *
42
+ * // Append chunks as they arrive
43
+ * manager.appendChunk('file-abc', Buffer.from('chunk1'))
44
+ * manager.appendChunk('file-abc', Buffer.from('chunk2'))
45
+ *
46
+ * // Downloader reads partial data
47
+ * const result = manager.get('file-abc', 0)
48
+ * console.log(result.data) // Buffer with 'chunk1chunk2'
49
+ * console.log(result.isComplete) // false
50
+ * console.log(result.totalReceived) // 12
51
+ *
52
+ * // Mark as complete when done
53
+ * manager.complete('file-abc')
54
+ *
55
+ * // Clean up on shutdown
56
+ * manager.destroy()
57
+ */
58
+
59
+ /**
60
+ * @typedef {Object} StreamingFileEntry
61
+ * Internal entry tracking a streaming file transfer.
62
+ *
63
+ * @property {string} uploaderId - Client ID of the uploader
64
+ * @property {Buffer[]} chunks - Array of received data chunks
65
+ * @property {number} totalReceived - Total bytes received so far
66
+ * @property {boolean} isComplete - Whether the upload is complete
67
+ * @property {number} createdAt - Timestamp when the file was registered
68
+ * @property {NodeJS.Timeout} timer - Cleanup timeout timer
69
+ */
70
+
71
+ /**
72
+ * @typedef {Object} StreamingFileResult
73
+ * Result from reading a streaming file.
74
+ *
75
+ * @property {Buffer} data - The file data (or partial data from offset)
76
+ * @property {boolean} isComplete - Whether the upload is complete
77
+ * @property {number} totalReceived - Total bytes received so far
78
+ */
79
+
80
+ /**
81
+ * Manages streaming file transfers between clients.
82
+ *
83
+ * This class provides a buffer for streaming data that flows through the server
84
+ * from one client to another. It supports:
85
+ *
86
+ * - **Chunked uploads**: Data arrives in pieces and is accumulated
87
+ * - **Partial reads**: Downloaders can read data as it arrives
88
+ * - **Completion tracking**: Know when the full file has been received
89
+ * - **Automatic cleanup**: Files are removed after configurable timeouts
90
+ *
91
+ * The manager uses a two-phase timeout system:
92
+ * 1. **Start timeout**: Maximum time to wait for first chunk after registration
93
+ * 2. **Complete timeout**: Maximum time to keep file after completion
94
+ *
95
+ * @class StreamingFileManager
96
+ *
97
+ * @example
98
+ * // Basic usage
99
+ * const manager = new StreamingFileManager(30000, 60000)
100
+ *
101
+ * // Register and stream
102
+ * manager.register('file-1', 'client-a')
103
+ * manager.appendChunk('file-1', chunk1)
104
+ * manager.appendChunk('file-1', chunk2)
105
+ * manager.complete('file-1')
106
+ *
107
+ * @example
108
+ * // Reading with offset (for resumable downloads)
109
+ * const result1 = manager.get('file-1', 0) // Get all data
110
+ * const result2 = manager.get('file-1', 100) // Get data from byte 100
111
+ *
112
+ * @example
113
+ * // Progressive download while upload continues
114
+ * let offset = 0
115
+ * const interval = setInterval(() => {
116
+ * const result = manager.get('file-1', offset)
117
+ * if (result) {
118
+ * sendToDownloader(result.data)
119
+ * offset += result.data.length
120
+ *
121
+ * if (result.isComplete && offset >= result.totalReceived) {
122
+ * clearInterval(interval)
123
+ * }
124
+ * }
125
+ * }, 100)
126
+ */
127
+ class StreamingFileManager {
128
+ /**
129
+ * Creates a new StreamingFileManager instance.
130
+ *
131
+ * @constructor
132
+ * @param {number} startTimeout - Milliseconds before an idle (no data) file is cleaned up.
133
+ * This timeout starts when the file is registered and resets when data arrives.
134
+ * @param {number} completeTimeout - Milliseconds to keep a completed file before cleanup.
135
+ * After `complete()` is called, this timeout determines how long the file stays available.
136
+ *
137
+ * @example
138
+ * // Short timeouts for real-time streaming
139
+ * const rtManager = new StreamingFileManager(5000, 10000)
140
+ *
141
+ * @example
142
+ * // Longer timeouts for large file transfers
143
+ * const fileManager = new StreamingFileManager(60000, 300000)
144
+ */
145
+ constructor(startTimeout, completeTimeout) {
146
+ /**
147
+ * Timeout for idle files (no data received).
148
+ * @type {number}
149
+ * @private
150
+ */
151
+ this.startTimeout = startTimeout;
152
+
153
+ /**
154
+ * Timeout for completed files.
155
+ * @type {number}
156
+ * @private
157
+ */
158
+ this.completeTimeout = completeTimeout;
159
+
160
+ /**
161
+ * Map of active streaming files keyed by file ID.
162
+ * @type {Map<string, StreamingFileEntry>}
163
+ * @private
164
+ */
165
+ this.streamingFiles = new Map();
166
+ }
167
+
168
+ /**
169
+ * Registers a new streaming file for transfer.
170
+ *
171
+ * Creates an entry for the file and starts the cleanup timeout.
172
+ * If a file with the same ID already exists, it is replaced
173
+ * (the old timeout is cleared).
174
+ *
175
+ * @param {string} fileId - Unique identifier for the file
176
+ * @param {string} uploaderId - Client ID of the uploader (for authorization)
177
+ * @returns {string} The file ID (same as input, for chaining)
178
+ *
179
+ * @example
180
+ * // Register a new streaming file
181
+ * const fileId = manager.register('file-abc-123', 'client-uploader-id')
182
+ *
183
+ * @example
184
+ * // File ID can be any unique string
185
+ * manager.register(crypto.randomUUID(), clientId)
186
+ * manager.register(`${clientId}-${Date.now()}`, clientId)
187
+ */
188
+ register(fileId, uploaderId) {
189
+ // Clear existing entry if present
190
+ if (this.streamingFiles.has(fileId)) {
191
+ const existing = this.streamingFiles.get(fileId);
192
+ if (existing.timer) clearTimeout(existing.timer);
193
+ }
194
+
195
+ /**
196
+ * Create the streaming file entry.
197
+ * @type {StreamingFileEntry}
198
+ */
199
+ const entry = {
200
+ uploaderId,
201
+ chunks: [],
202
+ totalReceived: 0,
203
+ isComplete: false,
204
+ createdAt: Date.now(),
205
+ // Set cleanup timer for start + complete timeout combined
206
+ timer: setTimeout(() => {
207
+ this.streamingFiles.delete(fileId);
208
+ }, this.startTimeout + this.completeTimeout),
209
+ };
210
+
211
+ this.streamingFiles.set(fileId, entry);
212
+ return fileId;
213
+ }
214
+
215
+ /**
216
+ * Appends a data chunk to a streaming file.
217
+ *
218
+ * Chunks are accumulated in order and can be read by downloaders
219
+ * as they arrive. The total received byte count is updated.
220
+ *
221
+ * @param {string} fileId - The file's unique identifier
222
+ * @param {Buffer} chunk - Data chunk to append
223
+ * @returns {boolean} True if the chunk was appended, false if file not found
224
+ *
225
+ * @example
226
+ * // Append chunks as they arrive from the uploader
227
+ * socket.on('fileChunk', (fileId, data) => {
228
+ * const success = manager.appendChunk(fileId, Buffer.from(data))
229
+ * if (!success) {
230
+ * socket.emit('error', 'File not found')
231
+ * }
232
+ * })
233
+ *
234
+ * @example
235
+ * // Handle streaming upload from HTTP
236
+ * req.on('data', (chunk) => {
237
+ * manager.appendChunk(fileId, chunk)
238
+ * })
239
+ */
240
+ appendChunk(fileId, chunk) {
241
+ const entry = this.streamingFiles.get(fileId);
242
+ if (!entry) return false;
243
+
244
+ entry.chunks.push(chunk);
245
+ entry.totalReceived += chunk.length;
246
+ return true;
247
+ }
248
+
249
+ /**
250
+ * Marks a streaming file as complete.
251
+ *
252
+ * Once complete, no more chunks should be appended. The file remains
253
+ * available for download until the complete timeout expires.
254
+ *
255
+ * Optionally, the complete data can be provided to replace all chunks.
256
+ * This is useful when the final assembled file differs from the chunks
257
+ * (e.g., after decompression or decryption).
258
+ *
259
+ * @param {string} fileId - The file's unique identifier
260
+ * @param {Buffer} [data] - Optional complete data to replace chunks
261
+ * @returns {boolean} True if marked complete, false if file not found
262
+ *
263
+ * @example
264
+ * // Mark complete without replacing data
265
+ * manager.complete('file-abc')
266
+ *
267
+ * @example
268
+ * // Mark complete with final assembled data
269
+ * const finalData = assembleChunks(manager.get('file-abc').data)
270
+ * manager.complete('file-abc', finalData)
271
+ *
272
+ * @example
273
+ * // In upload completion handler
274
+ * socket.on('fileComplete', (fileId) => {
275
+ * if (manager.complete(fileId)) {
276
+ * notifyDownloaders(fileId, 'complete')
277
+ * }
278
+ * })
279
+ */
280
+ complete(fileId, data) {
281
+ const entry = this.streamingFiles.get(fileId);
282
+ if (!entry) return false;
283
+
284
+ // Replace chunks with complete data if provided
285
+ if (data) {
286
+ entry.chunks = [data];
287
+ entry.totalReceived = data.length;
288
+ }
289
+
290
+ entry.isComplete = true;
291
+
292
+ // Reset timer for complete timeout only
293
+ clearTimeout(entry.timer);
294
+ entry.timer = setTimeout(() => {
295
+ this.streamingFiles.delete(fileId);
296
+ }, this.completeTimeout);
297
+
298
+ return true;
299
+ }
300
+
301
+ /**
302
+ * Retrieves data from a streaming file.
303
+ *
304
+ * Returns the accumulated data, completion status, and total bytes received.
305
+ * An optional offset allows reading only new data since last read
306
+ * (useful for progressive downloads).
307
+ *
308
+ * @param {string} fileId - The file's unique identifier
309
+ * @param {number} [offset=0] - Byte offset to start reading from
310
+ * @returns {StreamingFileResult|null} File data and status, or null if not found
311
+ *
312
+ * @example
313
+ * // Get all data
314
+ * const result = manager.get('file-abc')
315
+ * if (result) {
316
+ * console.log('Data:', result.data)
317
+ * console.log('Complete:', result.isComplete)
318
+ * console.log('Total:', result.totalReceived)
319
+ * }
320
+ *
321
+ * @example
322
+ * // Progressive download with offset
323
+ * let downloaded = 0
324
+ * function downloadMore() {
325
+ * const result = manager.get('file-abc', downloaded)
326
+ * if (result && result.data.length > 0) {
327
+ * writeToFile(result.data)
328
+ * downloaded += result.data.length
329
+ * }
330
+ * if (!result?.isComplete) {
331
+ * setTimeout(downloadMore, 100)
332
+ * }
333
+ * }
334
+ *
335
+ * @example
336
+ * // HTTP streaming response
337
+ * app.get('/stream/:fileId', (req, res) => {
338
+ * const result = manager.get(req.params.fileId)
339
+ * if (!result) {
340
+ * return res.status(404).send('Not found')
341
+ * }
342
+ * res.setHeader('X-Complete', result.isComplete ? '1' : '0')
343
+ * res.setHeader('X-Total', result.totalReceived)
344
+ * res.send(result.data)
345
+ * })
346
+ */
347
+ get(fileId, offset = 0) {
348
+ const entry = this.streamingFiles.get(fileId);
349
+ if (!entry) return null;
350
+
351
+ // Concatenate all chunks into a single buffer
352
+ const data = Buffer.concat(entry.chunks);
353
+
354
+ return {
355
+ // Return data from offset (for partial reads)
356
+ data: offset > 0 ? data.slice(offset) : data,
357
+ isComplete: entry.isComplete,
358
+ totalReceived: entry.totalReceived,
359
+ };
360
+ }
361
+
362
+ /**
363
+ * Checks if a streaming file exists.
364
+ *
365
+ * @param {string} fileId - The file's unique identifier
366
+ * @returns {boolean} True if the file exists, false otherwise
367
+ *
368
+ * @example
369
+ * if (manager.has('file-abc')) {
370
+ * // File exists, safe to read or append
371
+ * }
372
+ */
373
+ has(fileId) {
374
+ return this.streamingFiles.has(fileId);
375
+ }
376
+
377
+ /**
378
+ * Cleans up files older than the specified maximum age.
379
+ *
380
+ * This is useful for periodic cleanup in addition to the automatic
381
+ * timeout-based cleanup. Can be called on an interval to ensure
382
+ * stale files don't accumulate.
383
+ *
384
+ * @param {number} maxAge - Maximum age in milliseconds
385
+ *
386
+ * @example
387
+ * // Clean up files older than 5 minutes every minute
388
+ * setInterval(() => {
389
+ * manager.cleanup(5 * 60 * 1000)
390
+ * }, 60 * 1000)
391
+ *
392
+ * @example
393
+ * // One-time cleanup of very old files
394
+ * manager.cleanup(24 * 60 * 60 * 1000) // 24 hours
395
+ */
396
+ cleanup(maxAge) {
397
+ const now = Date.now();
398
+ for (const [fileId, entry] of this.streamingFiles) {
399
+ /* istanbul ignore next 4 - periodic cleanup, covered by automatic timers in practice */
400
+ if (now - entry.createdAt > maxAge) {
401
+ clearTimeout(entry.timer);
402
+ this.streamingFiles.delete(fileId);
403
+ }
404
+ }
405
+ }
406
+
407
+ /**
408
+ * Destroys the manager and clears all streaming files.
409
+ *
410
+ * Cancels all pending cleanup timers and removes all files from memory.
411
+ * Call this when shutting down the server or when the manager is no
412
+ * longer needed.
413
+ *
414
+ * @example
415
+ * // Clean shutdown
416
+ * process.on('SIGTERM', () => {
417
+ * manager.destroy()
418
+ * process.exit(0)
419
+ * })
420
+ *
421
+ * @example
422
+ * // In test cleanup
423
+ * afterEach(() => {
424
+ * manager.destroy()
425
+ * })
426
+ */
427
+ destroy() {
428
+ for (const entry of this.streamingFiles.values()) {
429
+ clearTimeout(entry.timer);
430
+ }
431
+ this.streamingFiles.clear();
432
+ }
433
+ }
434
+
435
+ module.exports = { StreamingFileManager };