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,43 @@
1
+ # Server Client Module
2
+
3
+ ## Overview
4
+
5
+ The server client module enables api-ape servers to act as WebSocket clients, connecting outbound to other api-ape servers or WebSocket endpoints. This is essential for server-to-server communication in distributed architectures.
6
+
7
+ **Key capabilities:**
8
+
9
+ - **Outbound connections** — Connect to other api-ape servers or WebSocket endpoints
10
+ - **Proxy-based API** — Same `api.users.list()` syntax as the browser client
11
+ - **Auto-reconnection** — Automatic reconnection with exponential backoff
12
+ - **Message queuing** — Queues messages during disconnection periods
13
+ - **JSS encoding** — Full support for Date, Set, Map, and other extended types
14
+
15
+ The client provides the same proxy-based API as the browser client (`api.users.list()`), making server-to-server calls feel like local function calls.
16
+
17
+ > **Contributing?** See [`files.md`](./files.md) for directory structure and file descriptions.
18
+
19
+ ## Usage
20
+
21
+ ```js
22
+ const { api } = require('api-ape/server/client')
23
+
24
+ // Connect to another api-ape server
25
+ const remote = api('ws://other-server:3000/api/ape')
26
+
27
+ // Call remote endpoints (returns Promise)
28
+ const users = await remote.users.list({ limit: 10 })
29
+
30
+ // Listen for broadcasts from remote server
31
+ remote.on('notification', ({ data }) => {
32
+ console.log('Remote notification:', data)
33
+ })
34
+
35
+ // Disconnect when done
36
+ remote.disconnect()
37
+ ```
38
+
39
+ ## See Also
40
+
41
+ - [`../README.md`](../README.md) — Main server documentation
42
+ - [`../../client/README.md`](../../client/README.md) — Browser client documentation
43
+ - [`../adapters/README.md`](../adapters/README.md) — Forest distributed mesh (alternative for multi-server)
@@ -0,0 +1,586 @@
1
+ /**
2
+ * @fileoverview Client Connection Management for api-ape Node.js Client
3
+ *
4
+ * This module provides WebSocket connection management for the server-side
5
+ * api-ape client. It handles:
6
+ *
7
+ * - WebSocket connection lifecycle (connect, disconnect, reconnect)
8
+ * - Connection state tracking and notifications
9
+ * - Message sending with request/response correlation
10
+ * - Event subscription (typed and untyped)
11
+ * - Request queuing during disconnection
12
+ *
13
+ * The connection automatically reconnects on disconnection unless explicitly
14
+ * closed via `close()`. Requests made while disconnected are queued and
15
+ * sent once the connection is re-established.
16
+ *
17
+ * @module server/client/connection
18
+ * @see {@link module:server/client} - Main client module
19
+ * @see {@link module:utils/jss} - JSON SuperSet encoding/decoding
20
+ *
21
+ * @example
22
+ * const { connect, close, on, onConnectionChange, ConnectionState } = require('./connection')
23
+ *
24
+ * // Establish connection
25
+ * connect('localhost', 3000)
26
+ *
27
+ * // Monitor connection state
28
+ * onConnectionChange(state => {
29
+ * console.log('State:', state)
30
+ * })
31
+ *
32
+ * // Subscribe to events
33
+ * on('message', data => {
34
+ * console.log('Received:', data)
35
+ * })
36
+ */
37
+
38
+ const jss = require("../../utils/jss");
39
+ const { WebSocket: WsPolyfill } = require("../lib/ws");
40
+
41
+ /**
42
+ * WebSocket constructor - uses native if available, falls back to polyfill.
43
+ * @private
44
+ * @type {typeof WebSocket}
45
+ */
46
+ const WebSocket = globalThis.WebSocket || WsPolyfill;
47
+
48
+ /**
49
+ * Connection state enumeration.
50
+ * Represents the possible states of the WebSocket connection.
51
+ *
52
+ * @readonly
53
+ * @enum {string}
54
+ * @property {string} Disconnected - Not connected to server
55
+ * @property {string} Connecting - Connection attempt in progress
56
+ * @property {string} Connected - Successfully connected and ready
57
+ * @property {string} Closing - Connection is being gracefully closed
58
+ *
59
+ * @example
60
+ * const { ConnectionState, onConnectionChange } = require('./connection')
61
+ *
62
+ * onConnectionChange(state => {
63
+ * if (state === ConnectionState.Connected) {
64
+ * console.log('Ready to communicate')
65
+ * }
66
+ * })
67
+ */
68
+ const ConnectionState = {
69
+ Disconnected: "disconnected",
70
+ Connecting: "connecting",
71
+ Connected: "connected",
72
+ Closing: "closing",
73
+ };
74
+
75
+ // ============================================================================
76
+ // INTERNAL STATE
77
+ // ============================================================================
78
+
79
+ /**
80
+ * Active WebSocket connection instance.
81
+ * @private
82
+ * @type {WebSocket|null}
83
+ */
84
+ let ws = null;
85
+
86
+ /**
87
+ * Current connection state.
88
+ * @private
89
+ * @type {string}
90
+ */
91
+ let connectionState = ConnectionState.Disconnected;
92
+
93
+ /**
94
+ * Array of connection state change listeners.
95
+ * @private
96
+ * @type {Array<function(string): void>}
97
+ */
98
+ const connectionChangeListeners = [];
99
+
100
+ /**
101
+ * Map of pending request callbacks keyed by query ID.
102
+ * Each callback receives (error, result) when the server responds.
103
+ * @private
104
+ * @type {Object<string, function(Error|null, *): void>}
105
+ */
106
+ const waitingOn = {};
107
+
108
+ /**
109
+ * Array of general message receivers (handles all message types).
110
+ * @private
111
+ * @type {Array<function({err: *, type: string, data: *}): void>}
112
+ */
113
+ const receiverArray = [];
114
+
115
+ /**
116
+ * Map of typed message receivers keyed by message type.
117
+ * @private
118
+ * @type {Object<string, Array<function({err: *, type: string, data: *}): void>>}
119
+ */
120
+ const ofTypesOb = {};
121
+
122
+ /**
123
+ * Counter for generating unique query IDs.
124
+ * @private
125
+ * @type {number}
126
+ */
127
+ let queryCounter = 0;
128
+
129
+ /**
130
+ * Queue of requests waiting to be sent when connection is established.
131
+ * @private
132
+ * @type {Array<{type: string, data: *, resolve: function, reject: function, createdAt: number, timer: NodeJS.Timeout}>}
133
+ */
134
+ let bufferedCalls = [];
135
+
136
+ /**
137
+ * Queue of receivers waiting to be registered when connection is established.
138
+ * @private
139
+ * @type {Array<{type: string|null, handler: function}>}
140
+ */
141
+ let bufferedReceivers = [];
142
+
143
+ /**
144
+ * Whether the connection is ready to send messages.
145
+ * @private
146
+ * @type {boolean}
147
+ */
148
+ let ready = false;
149
+
150
+ /**
151
+ * Whether auto-reconnect is enabled.
152
+ * Disabled by calling close(), re-enabled by calling connect().
153
+ * @private
154
+ * @type {boolean}
155
+ */
156
+ let reconnectEnabled = true;
157
+
158
+ /**
159
+ * Timer ID for reconnect delay.
160
+ * @private
161
+ * @type {NodeJS.Timeout|null}
162
+ */
163
+ let reconnectTimer = null;
164
+
165
+ /**
166
+ * Server WebSocket URL.
167
+ * Can be set via APE_SERVER environment variable or connect() arguments.
168
+ * @private
169
+ * @type {string|null}
170
+ */
171
+ let serverUrl = process.env.APE_SERVER || null;
172
+
173
+ // ============================================================================
174
+ // CONFIGURATION CONSTANTS
175
+ // ============================================================================
176
+
177
+ /**
178
+ * Timeout for initial connection in milliseconds.
179
+ * Queued requests will be rejected after this time if connection isn't established.
180
+ * @private
181
+ * @constant {number}
182
+ */
183
+ const connectTimeout = 5000;
184
+
185
+ /**
186
+ * Total timeout for a request in milliseconds.
187
+ * Includes time spent waiting for server response.
188
+ * @private
189
+ * @constant {number}
190
+ */
191
+ const totalRequestTimeout = 10000;
192
+
193
+ // ============================================================================
194
+ // INTERNAL FUNCTIONS
195
+ // ============================================================================
196
+
197
+ /**
198
+ * Notifies all listeners of a connection state change.
199
+ * Only triggers if the state actually changed.
200
+ *
201
+ * @private
202
+ * @function notifyConnectionChange
203
+ * @param {string} newState - The new connection state
204
+ */
205
+ function notifyConnectionChange(newState) {
206
+ if (connectionState !== newState) {
207
+ connectionState = newState;
208
+ connectionChangeListeners.forEach((fn) => fn(newState));
209
+ }
210
+ }
211
+
212
+ /**
213
+ * Generates a unique query ID for request/response correlation.
214
+ * Format: "q{timestamp36}_{counter36}" for compactness and uniqueness.
215
+ *
216
+ * @private
217
+ * @function generateQueryId
218
+ * @returns {string} Unique query identifier
219
+ *
220
+ * @example
221
+ * generateQueryId() // "qlxyz123_0"
222
+ * generateQueryId() // "qlxyz123_1"
223
+ */
224
+ function generateQueryId() {
225
+ return `q${Date.now().toString(36)}_${(queryCounter++).toString(36)}`;
226
+ }
227
+
228
+ /**
229
+ * Sends a message over the WebSocket and returns a promise for the response.
230
+ *
231
+ * The message is assigned a unique query ID that correlates the request
232
+ * with the server's response. A timeout ensures the promise doesn't hang
233
+ * indefinitely if the server doesn't respond.
234
+ *
235
+ * @private
236
+ * @function send
237
+ * @param {string} type - The message type (API path)
238
+ * @param {*} data - The request payload
239
+ * @param {number} [createdAt=Date.now()] - Timestamp for timeout calculation
240
+ * @returns {Promise<*>} Promise resolving to the server's response data
241
+ * @throws {Error} If the request times out
242
+ *
243
+ * @example
244
+ * const result = await send('/users/list', { limit: 10 })
245
+ */
246
+ function send(type, data, createdAt = Date.now()) {
247
+ const queryId = generateQueryId();
248
+ return new Promise((resolve, reject) => {
249
+ // Set up timeout for server response
250
+ const timer = setTimeout(() => {
251
+ delete waitingOn[queryId];
252
+ reject(new Error(`Request timeout: ${type}`));
253
+ }, totalRequestTimeout);
254
+
255
+ // Register callback for when response arrives
256
+ waitingOn[queryId] = (err, result) => {
257
+ clearTimeout(timer);
258
+ if (err) reject(typeof err === "string" ? new Error(err) : err);
259
+ else resolve(result);
260
+ };
261
+
262
+ // Send the message with JSS encoding
263
+ ws.send(jss.stringify({ type, data, queryId, createdAt }));
264
+ });
265
+ }
266
+
267
+ /**
268
+ * Registers a message receiver for a specific type or all messages.
269
+ *
270
+ * @private
271
+ * @function setOnReceiver
272
+ * @param {string|null} type - Message type to listen for, or null for all
273
+ * @param {function} handler - Callback function for received messages
274
+ */
275
+ function setOnReceiver(type, handler) {
276
+ if (type === null) {
277
+ receiverArray.push(handler);
278
+ } else {
279
+ if (!ofTypesOb[type]) ofTypesOb[type] = [];
280
+ ofTypesOb[type].push(handler);
281
+ }
282
+ }
283
+
284
+ // ============================================================================
285
+ // PUBLIC FUNCTIONS
286
+ // ============================================================================
287
+
288
+ /**
289
+ * Establishes a WebSocket connection to the api-ape server.
290
+ *
291
+ * If host and port are provided, constructs the WebSocket URL.
292
+ * Otherwise, uses the APE_SERVER environment variable.
293
+ *
294
+ * The connection:
295
+ * - Auto-reconnects on disconnection (unless close() was called)
296
+ * - Processes buffered receivers and queued requests on connect
297
+ * - Parses incoming messages with JSS and routes to handlers
298
+ *
299
+ * @function connect
300
+ * @param {string} [host] - Server hostname (e.g., 'localhost')
301
+ * @param {number} [port] - Server port (e.g., 3000)
302
+ *
303
+ * @example
304
+ * // Connect with explicit host and port
305
+ * connect('localhost', 3000)
306
+ *
307
+ * @example
308
+ * // Connect using APE_SERVER environment variable
309
+ * process.env.APE_SERVER = 'ws://api.example.com/api/ape'
310
+ * connect()
311
+ */
312
+ function connect(host, port) {
313
+ // Build URL from arguments if provided
314
+ if (typeof host === "string" && typeof port === "number") {
315
+ serverUrl = `ws://${host}:${port}/api/ape`;
316
+ }
317
+ if (!serverUrl) return;
318
+
319
+ // Don't create duplicate connections
320
+ if (ws && ws.readyState !== WebSocket.CLOSED) return;
321
+
322
+ notifyConnectionChange(ConnectionState.Connecting);
323
+ ws = new WebSocket(serverUrl);
324
+
325
+ /**
326
+ * Handle successful connection.
327
+ * Registers buffered receivers and sends queued requests.
328
+ */
329
+ ws.onopen = () => {
330
+ ready = true;
331
+ notifyConnectionChange(ConnectionState.Connected);
332
+
333
+ // Register any receivers that were added while disconnected
334
+ bufferedReceivers.forEach(({ type, handler }) =>
335
+ setOnReceiver(type, handler),
336
+ );
337
+ bufferedReceivers = [];
338
+
339
+ // Send any requests that were queued while disconnected
340
+ bufferedCalls.forEach(
341
+ ({ type, data, resolve, reject, createdAt, timer }) => {
342
+ clearTimeout(timer);
343
+ send(type, data, createdAt).then(resolve).catch(reject);
344
+ },
345
+ );
346
+ bufferedCalls = [];
347
+ };
348
+
349
+ /**
350
+ * Handle incoming messages.
351
+ * Routes responses to waiting callbacks, broadcasts to receivers.
352
+ */
353
+ ws.onmessage = (event) => {
354
+ const msg = jss.parse(
355
+ typeof event.data === "string" ? event.data : event.data.toString(),
356
+ );
357
+ const { err, type, queryId, data } = msg;
358
+
359
+ // If this is a response to a pending request, invoke the callback
360
+ if (queryId && waitingOn[queryId]) {
361
+ waitingOn[queryId](err, data);
362
+ delete waitingOn[queryId];
363
+ return;
364
+ }
365
+
366
+ // Otherwise, broadcast to type-specific receivers
367
+ if (ofTypesOb[type]) ofTypesOb[type].forEach((h) => h({ err, type, data }));
368
+
369
+ // And to general receivers
370
+ receiverArray.forEach((h) => h({ err, type, data }));
371
+ };
372
+
373
+ /**
374
+ * Handle WebSocket errors.
375
+ * Logs the error but doesn't close the connection (onclose will fire).
376
+ */
377
+ ws.onerror = (err) =>
378
+ console.error("🦍 api-ape client error:", err.message || err);
379
+
380
+ /**
381
+ * Handle connection close.
382
+ * Triggers auto-reconnect after delay if enabled.
383
+ */
384
+ ws.onclose = () => {
385
+ ready = false;
386
+ ws = null;
387
+ notifyConnectionChange(ConnectionState.Disconnected);
388
+
389
+ // Auto-reconnect after 1 second if not explicitly closed
390
+ if (reconnectEnabled && serverUrl) {
391
+ reconnectTimer = setTimeout(() => connect(), 1000);
392
+ }
393
+ };
394
+ }
395
+
396
+ /**
397
+ * Closes the WebSocket connection and disables auto-reconnect.
398
+ *
399
+ * Call this when you want to cleanly shut down the connection.
400
+ * To re-enable auto-reconnect, call connect() again.
401
+ *
402
+ * @function close
403
+ *
404
+ * @example
405
+ * // Clean shutdown
406
+ * process.on('SIGTERM', () => {
407
+ * close()
408
+ * process.exit(0)
409
+ * })
410
+ */
411
+ function close() {
412
+ reconnectEnabled = false;
413
+ if (reconnectTimer) {
414
+ clearTimeout(reconnectTimer);
415
+ reconnectTimer = null;
416
+ }
417
+ if (ws) {
418
+ notifyConnectionChange(ConnectionState.Closing);
419
+ ws.close();
420
+ }
421
+ }
422
+
423
+ /**
424
+ * Queues a request or sends it immediately if connected.
425
+ *
426
+ * When connected, immediately sends the request.
427
+ * When disconnected, queues the request to be sent on connection.
428
+ * Queued requests timeout after connectTimeout milliseconds.
429
+ *
430
+ * @function queueOrSend
431
+ * @param {string} type - The message type (API path)
432
+ * @param {*} data - The request payload
433
+ * @returns {Promise<*>} Promise resolving to the server's response
434
+ * @throws {Error} If connection times out while queued
435
+ *
436
+ * @example
437
+ * // Will send immediately if connected, or queue if not
438
+ * const users = await queueOrSend('/users/list', { limit: 10 })
439
+ */
440
+ function queueOrSend(type, data) {
441
+ // If connected, send immediately
442
+ if (ready && ws && ws.readyState === WebSocket.OPEN) {
443
+ return send(type, data);
444
+ }
445
+
446
+ // Otherwise, queue for later
447
+ return new Promise((resolve, reject) => {
448
+ const createdAt = Date.now();
449
+
450
+ // Set up connection timeout
451
+ const timer = setTimeout(() => {
452
+ const idx = bufferedCalls.findIndex((m) => m.createdAt === createdAt);
453
+ if (idx > -1) bufferedCalls.splice(idx, 1);
454
+ reject(new Error(`Connection timeout: ${type}`));
455
+ }, connectTimeout);
456
+
457
+ // Add to queue
458
+ bufferedCalls.push({ type, data, resolve, reject, createdAt, timer });
459
+
460
+ // Trigger connection if not already connecting
461
+ if (connectionState === ConnectionState.Disconnected && serverUrl) {
462
+ connect();
463
+ }
464
+ });
465
+ }
466
+
467
+ /**
468
+ * Subscribes to server-sent events.
469
+ *
470
+ * @function on
471
+ * @param {string|function} type - Event type to listen for, or handler for all events
472
+ * @param {function} [handler] - Handler function (if type is a string)
473
+ *
474
+ * @example
475
+ * // Listen for specific event type
476
+ * on('notification', (data) => {
477
+ * console.log('Notification:', data)
478
+ * })
479
+ *
480
+ * @example
481
+ * // Listen for all events
482
+ * on((event) => {
483
+ * console.log('Event:', event.type, event.data)
484
+ * })
485
+ */
486
+ function on(type, handler) {
487
+ // Support on(handler) syntax for listening to all events
488
+ if (typeof type === "function") {
489
+ handler = type;
490
+ type = null;
491
+ }
492
+
493
+ // If connected, register immediately
494
+ if (ready) {
495
+ setOnReceiver(type, handler);
496
+ } else {
497
+ // Otherwise, buffer for when connection opens
498
+ bufferedReceivers.push({ type, handler });
499
+
500
+ // Trigger connection if we have a server URL
501
+ if (serverUrl) connect();
502
+ }
503
+ }
504
+
505
+ /**
506
+ * Subscribes to connection state changes.
507
+ *
508
+ * The handler is called immediately with the current state,
509
+ * and then again whenever the state changes.
510
+ *
511
+ * @function onConnectionChange
512
+ * @param {function(string): void} handler - Callback receiving ConnectionState values
513
+ * @returns {function(): void} Unsubscribe function
514
+ *
515
+ * @example
516
+ * const unsubscribe = onConnectionChange((state) => {
517
+ * console.log('Connection state:', state)
518
+ * })
519
+ *
520
+ * // Later, stop listening
521
+ * unsubscribe()
522
+ */
523
+ function onConnectionChange(handler) {
524
+ connectionChangeListeners.push(handler);
525
+
526
+ // Immediately invoke with current state
527
+ handler(connectionState);
528
+
529
+ // Return unsubscribe function
530
+ return () => {
531
+ const idx = connectionChangeListeners.indexOf(handler);
532
+ if (idx > -1) connectionChangeListeners.splice(idx, 1);
533
+ };
534
+ }
535
+
536
+ /**
537
+ * Checks if the connection is ready to send messages.
538
+ *
539
+ * @function isReady
540
+ * @returns {boolean} True if connected and ready
541
+ */
542
+ function isReady() {
543
+ return ready;
544
+ }
545
+
546
+ /**
547
+ * Gets the current WebSocket instance.
548
+ * Useful for advanced use cases like accessing readyState directly.
549
+ *
550
+ * @function getWs
551
+ * @returns {WebSocket|null} The WebSocket instance, or null if not connected
552
+ */
553
+ function getWs() {
554
+ return ws;
555
+ }
556
+
557
+ // ============================================================================
558
+ // EXPORTS
559
+ // ============================================================================
560
+
561
+ module.exports = {
562
+ /** Connection state enumeration */
563
+ ConnectionState,
564
+ /** Establish connection to server */
565
+ connect,
566
+ /** Close connection and disable auto-reconnect */
567
+ close,
568
+ /** Send a message (internal, requires active connection) */
569
+ send,
570
+ /** Queue or send a message */
571
+ queueOrSend,
572
+ /** Subscribe to server events */
573
+ on,
574
+ /** Subscribe to connection state changes */
575
+ onConnectionChange,
576
+ /** Register a message receiver (internal) */
577
+ setOnReceiver,
578
+ /** Notify connection state change (internal) */
579
+ notifyConnectionChange,
580
+ /** Check if connection is ready */
581
+ isReady,
582
+ /** Get WebSocket instance */
583
+ getWs,
584
+ /** WebSocket constructor (native or polyfill) */
585
+ WebSocket,
586
+ };
@@ -0,0 +1,40 @@
1
+ # Server Client Module Files
2
+
3
+ This module enables api-ape servers to act as WebSocket clients, connecting outbound to other api-ape servers or WebSocket endpoints. Essential for server-to-server communication in distributed architectures.
4
+
5
+ ## Guidelines
6
+
7
+ - **Mirror browser client API** — The proxy-based API (`api.users.list()`) must behave identically to the browser client
8
+ - **JSS encoding** — Use `utils/jss` for message serialization to preserve Date, Set, Map, etc.
9
+ - **Auto-reconnection** — Always implement exponential backoff on connection failures
10
+ - **Message queuing** — Buffer messages during disconnection; deliver when reconnected
11
+ - **QueryId correlation** — All requests must track `queryId` for proper response matching
12
+
13
+ ## Directory Structure
14
+
15
+ ```
16
+ client/
17
+ ├── index.js # Main entry point (proxy-based API client)
18
+ └── connection.js # Client connection management
19
+ ```
20
+
21
+ ## Files
22
+
23
+ ### `index.js`
24
+
25
+ Main entry point for the server-side WebSocket client. Provides the proxy-based API that mirrors the browser client:
26
+
27
+ - **Proxy handler** — Intercepts property access to build API paths dynamically (`api.users.list()`)
28
+ - **Reserved methods** — Exposes `connect`, `close`, `on`, `onConnectionChange`, and `transport`
29
+ - **Module exports** — Exports the proxy client, individual methods, and `ConnectionState` enum
30
+
31
+ ### `connection.js`
32
+
33
+ Manages outbound WebSocket connections from the server:
34
+
35
+ - **Connection lifecycle** — Connect, disconnect, and reconnect handling
36
+ - **Exponential backoff** — Automatic reconnection with increasing delays
37
+ - **Message queuing** — Queues messages during disconnection periods
38
+ - **JSS encoding/decoding** — Full support for extended types (Date, Set, Map, etc.)
39
+ - **Request/response correlation** — Tracks pending requests via `queryId`
40
+ - **Event emission** — Emits `message`, `open`, `close`, and `error` events