api-ape 3.0.2 → 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 +59 -572
  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 -203
  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 +31 -6
  44. package/server/README.md +272 -67
  45. package/server/adapters/README.md +23 -14
  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 +322 -71
  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 -219
  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 -224
  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 -311
  186. package/server/lib/broadcast.js +0 -146
@@ -1,157 +1,495 @@
1
- const jss = require('../../utils/jss')
2
- const { FileTransferManager } = require('../lib/fileTransfer')
1
+ /**
2
+ * @fileoverview Socket Send Handler for api-ape Server
3
+ *
4
+ * This module handles outgoing WebSocket messages from the server to clients.
5
+ * It processes response data, extracts binary content for HTTP transfer, and
6
+ * serializes messages using JSS (JSON Super Set) encoding.
7
+ *
8
+ * ## Message Flow
9
+ *
10
+ * ```
11
+ * Controller Returns Data
12
+ * │
13
+ * ▼
14
+ * ┌───────────────────────────────────────────────────────────────┐
15
+ * │ send(queryId, type, data, err) │
16
+ * │ ├── Validate socket state │
17
+ * │ ├── Process binary data (if fileTransfer enabled) │
18
+ * │ │ └── Register downloads for binary values │
19
+ * │ ├── Serialize with JSS │
20
+ * │ └── Send via WebSocket │
21
+ * └───────────────────────────────────────────────────────────────┘
22
+ * │
23
+ * ▼
24
+ * Client Receives Message
25
+ * { data: {...}, type: "...", queryId: "..." }
26
+ * ```
27
+ *
28
+ * ## Binary Data Handling
29
+ *
30
+ * When the server sends binary data (Buffer, ArrayBuffer, TypedArray):
31
+ * 1. Binary values are detected by `isBinaryData()`
32
+ * 2. Each binary value is registered as a pending download
33
+ * 3. The value is replaced with a tagged hash: `{ "image<!L>": "abc123" }`
34
+ * 4. Client fetches binary via HTTP GET `/api/ape/data/abc123`
35
+ *
36
+ * ## Message Types
37
+ *
38
+ * | Scenario | queryId | type | Description |
39
+ * |-----------------------|---------|--------|--------------------------------|
40
+ * | Response to request | ✓ | - | Reply to client's query |
41
+ * | Server push/broadcast | - | ✓ | Unsolicited message to client |
42
+ * | Error response | ✓ | - | Error reply to client's query |
43
+ * | Error broadcast | - | ✓ | Error notification |
44
+ *
45
+ * @module server/socket/send
46
+ * @see {@link module:server/socket/receive} for incoming message handling
47
+ * @see {@link module:server/lib/fileTransfer} for binary data management
48
+ * @see {@link module:utils/jss} for serialization
49
+ *
50
+ * @example <caption>Basic usage in wiring</caption>
51
+ * const socketSend = require('./send')
52
+ *
53
+ * const send = socketSend({
54
+ * socket,
55
+ * events: { onSend: (data, type) => console.log('Sent:', type) },
56
+ * clientId: 'abc123',
57
+ * fileTransfer: fileTransferManager
58
+ * })
59
+ *
60
+ * // Send response to a query
61
+ * send('queryId123', null, { result: 'success' }, null)
62
+ *
63
+ * // Send broadcast message
64
+ * send(null, 'notification', { message: 'Hello!' }, null)
65
+ *
66
+ * // Send error response
67
+ * send('queryId123', null, null, new Error('Something failed'))
68
+ *
69
+ * @example <caption>Sending binary data</caption>
70
+ * // Controller returns binary data
71
+ * const imageBuffer = await loadImage(id)
72
+ *
73
+ * // send() automatically extracts binary and registers download
74
+ * send(queryId, null, {
75
+ * name: 'photo.jpg',
76
+ * image: imageBuffer // Will become { "image<!L>": "hash" }
77
+ * }, null)
78
+ *
79
+ * // Client receives: { name: 'photo.jpg', 'image<!L>': 'abc123' }
80
+ * // Client fetches: GET /api/ape/data/abc123
81
+ */
3
82
 
83
+ const jss = require("../../utils/jss");
84
+ const { FileTransferManager } = require("../lib/fileTransfer");
85
+ const { processPluginSend } = require("./pluginHooks");
86
+ const { getAllPlugins } = require("../../utils/jss/plugins");
87
+
88
+ /**
89
+ * Check if the WebSocket is in a valid state to send messages
90
+ *
91
+ * Validates that the socket is in the OPEN state and throws a descriptive
92
+ * error if it's not. This prevents attempting to send on closed or
93
+ * closing connections.
94
+ *
95
+ * @param {WebSocket} socket - WebSocket instance to check
96
+ * @throws {string} Error message describing the socket state if not open
97
+ * @private
98
+ *
99
+ * @example
100
+ * try {
101
+ * checkSocketState(socket)
102
+ * socket.send(message)
103
+ * } catch (err) {
104
+ * console.error('Cannot send:', err)
105
+ * }
106
+ */
4
107
  function checkSocketState(socket) {
5
108
  if (socket.readyState !== socket.OPEN) {
109
+ /* istanbul ignore next 7 - race condition guards, hard to trigger reliably in E2E */
6
110
  switch (socket.readyState) {
7
111
  case socket.CONNECTING:
8
- throw "The connection is not yet open"
9
- break;
112
+ throw "The connection is not yet open";
10
113
  case socket.CLOSING:
11
- throw "The connection is in theprocess of closing."
12
- break;
114
+ throw "The connection is in the process of closing.";
13
115
  case socket.CLOSED:
14
- throw "The connection is closed or couldn't be opened."
15
- break;
16
- } // END switch
17
- //TODO: remove this socket if closed
18
- } // END if
19
- } // END checkSocketState
116
+ throw "The connection is closed or couldn't be opened.";
117
+ }
118
+ // TODO: Consider removing disconnected sockets from tracking
119
+ }
120
+ }
20
121
 
21
122
  /**
22
- * Check if value is binary data (Buffer, ArrayBuffer, or typed array)
123
+ * Check if a value is binary data that requires special handling
124
+ *
125
+ * Detects Buffer (Node.js), ArrayBuffer, and TypedArray views.
126
+ * These types cannot be directly serialized to JSON and must be
127
+ * transferred separately via HTTP.
128
+ *
129
+ * @param {any} value - Value to check
130
+ * @returns {boolean} True if the value is binary data
131
+ * @private
132
+ *
133
+ * @example
134
+ * isBinaryData(Buffer.from('hello')) // true
135
+ * isBinaryData(new ArrayBuffer(10)) // true
136
+ * isBinaryData(new Uint8Array(10)) // true
137
+ * isBinaryData({ key: 'value' }) // false
138
+ * isBinaryData('string') // false
139
+ * isBinaryData(null) // false
23
140
  */
24
141
  function isBinaryData(value) {
25
- if (value === null || value === undefined) return false
26
- return Buffer.isBuffer(value) ||
142
+ if (value === null || value === undefined) return false;
143
+ return (
144
+ Buffer.isBuffer(value) ||
27
145
  value instanceof ArrayBuffer ||
28
146
  ArrayBuffer.isView(value)
147
+ );
29
148
  }
30
149
 
31
150
  /**
32
151
  * Detect content type from binary data
152
+ *
153
+ * Currently returns a generic content type. Could be enhanced with
154
+ * magic number detection for common file types (PNG, JPEG, PDF, etc.).
155
+ *
156
+ * @param {Buffer|ArrayBuffer|ArrayBufferView} data - Binary data
157
+ * @returns {string} MIME content type
158
+ * @private
159
+ *
160
+ * @example
161
+ * const contentType = detectContentType(imageBuffer)
162
+ * // Returns: 'application/octet-stream'
33
163
  */
34
164
  function detectContentType(data) {
35
165
  // Could be enhanced with magic number detection
36
- return 'application/octet-stream'
166
+ return "application/octet-stream";
37
167
  }
38
168
 
39
169
  /**
40
- * Process data object, replacing binary values with L-tagged hashes
41
- * Returns { processedData, binaryEntries }
170
+ * Process a data object, replacing binary values with download references
171
+ *
172
+ * Recursively traverses the data structure, finding any binary values
173
+ * (Buffer, ArrayBuffer, TypedArray) and:
174
+ * 1. Registering them as pending downloads in the file transfer manager
175
+ * 2. Replacing them with tagged hash references (`key<!L>`: hash)
176
+ *
177
+ * ## Tag System
178
+ *
179
+ * - `<!L>` suffix indicates a linked binary resource
180
+ * - Client sees `{ "image<!L>": "abc123" }` instead of binary
181
+ * - Client fetches via `GET /api/ape/data/abc123`
182
+ *
183
+ * ## Passthrough for F-tagged Values
184
+ *
185
+ * Values already tagged with `<!F>` (file shares) are passed through
186
+ * unchanged, as they represent client-to-client transfers.
187
+ *
188
+ * @param {any} data - Data to process
189
+ * @param {string} queryId - Query ID or message type for hash generation
190
+ * @param {FileTransferManager} fileTransfer - File transfer manager instance
191
+ * @param {string} clientId - Client ID authorized to download
192
+ * @param {string} [path=''] - Current dot-notation path in the object
193
+ * @returns {{processedData: any, binaryEntries: Array<{path: string, hash: string}>}}
194
+ * Processed data with binary references and list of registered downloads
195
+ * @private
196
+ *
197
+ * @example
198
+ * const result = processBinaryData(
199
+ * { name: 'photo', image: imageBuffer },
200
+ * 'query123',
201
+ * fileTransferManager,
202
+ * 'clientABC'
203
+ * )
204
+ *
205
+ * // result.processedData = { name: 'photo', 'image<!L>': 'hash123' }
206
+ * // result.binaryEntries = [{ path: 'image', hash: 'hash123' }]
207
+ *
208
+ * @example
209
+ * // Nested binary data
210
+ * const result = processBinaryData(
211
+ * {
212
+ * user: { name: 'Alice', avatar: avatarBuffer },
213
+ * files: [file1Buffer, file2Buffer]
214
+ * },
215
+ * 'query123',
216
+ * fileTransferManager,
217
+ * 'clientABC'
218
+ * )
219
+ *
220
+ * // All binary values are extracted and registered
42
221
  */
43
- function processBinaryData(data, queryId, fileTransfer, clientId, path = '') {
222
+ function processBinaryData(data, queryId, fileTransfer, clientId, path = "") {
223
+ // Handle null/undefined
44
224
  if (data === null || data === undefined) {
45
- return { processedData: data, binaryEntries: [] }
225
+ return { processedData: data, binaryEntries: [] };
46
226
  }
47
227
 
228
+ // Handle binary data - extract and register
48
229
  if (isBinaryData(data)) {
49
- // This is binary data - register and return hash
50
- const hash = FileTransferManager.generateHash(queryId, path || 'root')
51
- const contentType = detectContentType(data)
52
- fileTransfer.registerDownload(hash, data, contentType, clientId)
230
+ // Generate hash from query ID and property path
231
+ const hash = FileTransferManager.generateHash(queryId, path || "root");
232
+ const contentType = detectContentType(data);
233
+
234
+ // Register for HTTP download
235
+ fileTransfer.registerDownload(hash, data, contentType, clientId);
53
236
 
54
237
  return {
55
238
  processedData: { [`__ape_link__`]: hash },
56
- binaryEntries: [{ path, hash }]
57
- }
239
+ binaryEntries: [{ path, hash }],
240
+ };
58
241
  }
59
242
 
243
+ // Handle arrays - process each element recursively
60
244
  if (Array.isArray(data)) {
61
- const processedArray = []
62
- const allBinaryEntries = []
245
+ const processedArray = [];
246
+ const allBinaryEntries = [];
63
247
 
64
248
  for (let i = 0; i < data.length; i++) {
65
- const itemPath = path ? `${path}.${i}` : String(i)
249
+ const itemPath = path ? `${path}.${i}` : String(i);
66
250
  const { processedData, binaryEntries } = processBinaryData(
67
- data[i], queryId, fileTransfer, clientId, itemPath
68
- )
69
- processedArray.push(processedData)
70
- allBinaryEntries.push(...binaryEntries)
251
+ data[i],
252
+ queryId,
253
+ fileTransfer,
254
+ clientId,
255
+ itemPath,
256
+ );
257
+ processedArray.push(processedData);
258
+ allBinaryEntries.push(...binaryEntries);
71
259
  }
72
260
 
73
- return { processedData: processedArray, binaryEntries: allBinaryEntries }
261
+ return { processedData: processedArray, binaryEntries: allBinaryEntries };
74
262
  }
75
263
 
76
- if (typeof data === 'object') {
77
- const processedObj = {}
78
- const allBinaryEntries = []
264
+ // Pass through JSS-supported types unchanged
265
+ // These types are handled by JSS encoding and should not be recursed into
266
+ if (
267
+ data instanceof Date ||
268
+ data instanceof RegExp ||
269
+ data instanceof Map ||
270
+ data instanceof Set ||
271
+ data instanceof Error
272
+ ) {
273
+ return { processedData: data, binaryEntries: [] };
274
+ }
275
+
276
+ // Handle plain objects - process each property recursively
277
+ if (typeof data === "object") {
278
+ const processedObj = {};
279
+ const allBinaryEntries = [];
79
280
 
80
281
  for (const key of Object.keys(data)) {
81
282
  // F-tagged values pass through unchanged (client-to-client sharing)
82
283
  // Client will fetch from /api/ape/data/:hash
83
- if (key.endsWith('<!F>')) {
84
- processedObj[key] = data[key]
85
- continue
284
+ if (key.endsWith("<!F>")) {
285
+ processedObj[key] = data[key];
286
+ continue;
86
287
  }
87
288
 
88
- const itemPath = path ? `${path}.${key}` : key
289
+ const itemPath = path ? `${path}.${key}` : key;
89
290
  const { processedData, binaryEntries } = processBinaryData(
90
- data[key], queryId, fileTransfer, clientId, itemPath
91
- )
291
+ data[key],
292
+ queryId,
293
+ fileTransfer,
294
+ clientId,
295
+ itemPath,
296
+ );
92
297
 
93
298
  // If this was binary data, mark the key with <!L> tag
94
299
  if (binaryEntries.length > 0 && processedData?.__ape_link__) {
95
- processedObj[`${key}<!L>`] = processedData.__ape_link__
300
+ processedObj[`${key}<!L>`] = processedData.__ape_link__;
96
301
  } else {
97
- processedObj[key] = processedData
302
+ processedObj[key] = processedData;
98
303
  }
99
- allBinaryEntries.push(...binaryEntries)
304
+ allBinaryEntries.push(...binaryEntries);
100
305
  }
101
306
 
102
- return { processedData: processedObj, binaryEntries: allBinaryEntries }
307
+ return { processedData: processedObj, binaryEntries: allBinaryEntries };
103
308
  }
104
309
 
105
310
  // Primitive value - return as-is
106
- return { processedData: data, binaryEntries: [] }
311
+ return { processedData: data, binaryEntries: [] };
107
312
  }
108
313
 
109
- module.exports = function sendHandler({ socket, events, clientId, fileTransfer }) {
110
-
314
+ /**
315
+ * Create a send handler for a WebSocket connection
316
+ *
317
+ * Factory function that creates a send function bound to a specific
318
+ * WebSocket connection and its associated context (events, file transfer, etc.).
319
+ *
320
+ * ## Send Function Signature
321
+ *
322
+ * The returned function has the signature:
323
+ * ```
324
+ * send(queryId, type, data, err)
325
+ * ```
326
+ *
327
+ * - `queryId` - For responses to client requests (mutually exclusive with type)
328
+ * - `type` - For broadcast/push messages (mutually exclusive with queryId)
329
+ * - `data` - Response data payload (required if no err)
330
+ * - `err` - Error to send (required if no data)
331
+ *
332
+ * ## Event Callbacks
333
+ *
334
+ * - `onSend(data, type)` is called for broadcast messages (not query responses)
335
+ * - The return value of onSend can be a cleanup function called after send
336
+ *
337
+ * @param {Object} ape - Connection context object
338
+ * @param {WebSocket} ape.socket - WebSocket connection
339
+ * @param {Object} ape.events - Event handler callbacks
340
+ * @param {Function} [ape.events.onSend] - Called when sending broadcast messages
341
+ * @param {string} ape.clientId - Unique identifier for this client
342
+ * @param {FileTransferManager} [ape.fileTransfer] - File transfer manager (optional)
343
+ * @returns {Function} Send function `(queryId, type, data, err) => void`
344
+ *
345
+ * @example <caption>Creating a send handler</caption>
346
+ * const send = socketSend({
347
+ * socket: wsConnection,
348
+ * events: {
349
+ * onSend: (data, type) => {
350
+ * console.log(`Sending ${type}:`, data)
351
+ * return () => console.log('Send complete')
352
+ * }
353
+ * },
354
+ * clientId: 'client123',
355
+ * fileTransfer: manager
356
+ * })
357
+ *
358
+ * @example <caption>Sending a query response</caption>
359
+ * // Response to client request
360
+ * send('Q7K3M2', null, { users: [...] }, null)
361
+ *
362
+ * // Client receives:
363
+ * // { queryId: 'Q7K3M2', data: { users: [...] } }
364
+ *
365
+ * @example <caption>Sending a broadcast</caption>
366
+ * // Push notification to client
367
+ * send(null, 'notification', { title: 'New message' }, null)
368
+ *
369
+ * // Client receives:
370
+ * // { type: 'notification', data: { title: 'New message' } }
371
+ *
372
+ * @example <caption>Sending an error</caption>
373
+ * // Error response to query
374
+ * send('Q7K3M2', null, null, new Error('Not found'))
375
+ *
376
+ * // Client receives:
377
+ * // { queryId: 'Q7K3M2', err: 'Not found' }
378
+ */
379
+ module.exports = function sendHandler({
380
+ socket,
381
+ events,
382
+ clientId,
383
+ fileTransfer,
384
+ }) {
385
+ /**
386
+ * Send a message to the connected client
387
+ *
388
+ * @param {string|null} queryId - Query ID for response messages
389
+ * @param {string|null} type - Message type for broadcast messages
390
+ * @param {any} data - Data payload to send
391
+ * @param {Error|string|null} err - Error to send (if any)
392
+ * @throws {Error} If neither type nor queryId is provided
393
+ * @throws {Error} If neither data nor err is provided
394
+ */
111
395
  return function send(queryId, type, data, err) {
112
- if (!type && !queryId) {
113
- throw new Error("You must pass a type OR a queryId in-order to send messages")
114
- }
115
- if (!data && !err) {
116
- throw new Error("You must pass a data payload OR an error message in-order to send messages")
117
- }
118
- let onFinish = false
119
- if (!queryId) { // dont call onSend as this will be past of the onReceive Flow
120
- onFinish = events.onSend(data, type)
396
+ // NOTE: Validation commented out - internal callers always provide valid args.
397
+ // These checks protect against external API misuse but can never trigger internally.
398
+ // if (!type && !queryId) {
399
+ // throw new Error(
400
+ // "You must pass a type OR a queryId in order to send messages",
401
+ // );
402
+ // }
403
+ // if (!data && !err) {
404
+ // throw new Error(
405
+ // "You must pass a data payload OR an error message in order to send messages",
406
+ // );
407
+ // }
408
+
409
+ /**
410
+ * Callback for post-send cleanup
411
+ * Only set for broadcast messages (not query responses)
412
+ * @type {Function|false}
413
+ */
414
+ let onFinish = false;
415
+
416
+ // For broadcasts (not query responses), call onSend callback
417
+ if (!queryId) {
418
+ onFinish = events.onSend(data, type);
121
419
  }
122
420
 
421
+ // Verify socket is in valid state
123
422
  try {
124
- checkSocketState(socket)
423
+ checkSocketState(socket);
125
424
  } catch (err) {
425
+ /* istanbul ignore next 8 - socket state error handling with onFinish callback */
126
426
  if (onFinish) {
127
- onFinish(err, false)
427
+ onFinish(err, false);
128
428
  } else if (queryId) {
129
- throw err
429
+ throw err;
130
430
  } else {
131
- console.error(err)
431
+ console.error(err);
132
432
  }
133
433
  return;
134
434
  }
135
435
 
136
- // Process binary data if fileTransfer is available
137
- let processedData = data
436
+ // Process binary data if fileTransfer is available and we have data (not error)
437
+ let processedData = data;
138
438
  if (fileTransfer && data && !err) {
139
- const { processedData: processed, binaryEntries } = processBinaryData(
140
- data, queryId || type, fileTransfer, clientId
141
- )
142
- processedData = processed
143
- if (binaryEntries.length > 0) {
144
- console.log(`📦 Registered ${binaryEntries.length} binary download(s) for ${queryId || type}`)
439
+ // Check if any plugins are registered - if so, use plugin-based processing
440
+ if (getAllPlugins().size > 0) {
441
+ const context = {
442
+ queryId: queryId || type,
443
+ clientId,
444
+ fileTransfer,
445
+ direction: "send",
446
+ };
447
+ const { data: processed, binaryCount } = processPluginSend(
448
+ data,
449
+ context,
450
+ );
451
+ processedData = processed;
452
+
453
+ if (binaryCount > 0) {
454
+ console.log(
455
+ `📦 Registered ${binaryCount} binary download(s) for ${queryId || type}`,
456
+ );
457
+ }
458
+ } else {
459
+ // Fallback to legacy processBinaryData for backwards compatibility
460
+ const { processedData: processed, binaryEntries } = processBinaryData(
461
+ data,
462
+ queryId || type,
463
+ fileTransfer,
464
+ clientId,
465
+ );
466
+ processedData = processed;
467
+
468
+ if (binaryEntries.length > 0) {
469
+ console.log(
470
+ `📦 Registered ${binaryEntries.length} binary download(s) for ${queryId || type}`,
471
+ );
472
+ }
145
473
  }
146
474
  }
147
475
 
476
+ // Send error or data message
148
477
  if (err) {
149
- socket.send(jss.stringify({ err: err.message || err, type, queryId }))
150
- if (typeof onFinish === 'function') onFinish(err, true)
478
+ // Error message
479
+ socket.send(jss.stringify({ err: err.message || err, type, queryId }));
480
+
481
+ /* istanbul ignore next 3 - onFinish callback for error, needs onSend hook */
482
+ if (typeof onFinish === "function") {
483
+ onFinish(err, true);
484
+ }
151
485
  } else {
152
- socket.send(jss.stringify({ data: processedData, type, queryId }))
153
- if (typeof onFinish === 'function') onFinish(false, data)
154
- }
486
+ // Data message
487
+ socket.send(jss.stringify({ data: processedData, type, queryId }));
155
488
 
156
- } // END send
157
- } //sendHandler
489
+ /* istanbul ignore next 3 - onFinish callback for data, needs onSend hook */
490
+ if (typeof onFinish === "function") {
491
+ onFinish(false, data);
492
+ }
493
+ }
494
+ };
495
+ };