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.
- package/README.md +58 -570
- package/client/README.md +73 -14
- package/client/auth/crypto/aead.js +214 -0
- package/client/auth/crypto/constants.js +32 -0
- package/client/auth/crypto/encoding.js +104 -0
- package/client/auth/crypto/files.md +27 -0
- package/client/auth/crypto/kdf.js +217 -0
- package/client/auth/crypto-utils.js +118 -0
- package/client/auth/files.md +52 -0
- package/client/auth/key-recovery.js +288 -0
- package/client/auth/recovery/constants.js +37 -0
- package/client/auth/recovery/files.md +23 -0
- package/client/auth/recovery/key-derivation.js +61 -0
- package/client/auth/recovery/sss-browser.js +189 -0
- package/client/auth/share-storage.js +205 -0
- package/client/auth/storage/constants.js +18 -0
- package/client/auth/storage/db.js +132 -0
- package/client/auth/storage/files.md +27 -0
- package/client/auth/storage/keys.js +173 -0
- package/client/auth/storage/shares.js +200 -0
- package/client/browser.js +190 -23
- package/client/connectSocket.js +418 -988
- package/client/connection/README.md +23 -0
- package/client/connection/fileDownload.js +256 -0
- package/client/connection/fileHandling.js +450 -0
- package/client/connection/fileUtils.js +346 -0
- package/client/connection/files.md +71 -0
- package/client/connection/messageHandler.js +105 -0
- package/client/connection/network.js +350 -0
- package/client/connection/proxy.js +233 -0
- package/client/connection/sender.js +333 -0
- package/client/connection/state.js +321 -0
- package/client/connection/subscriptions.js +151 -0
- package/client/files.md +53 -0
- package/client/index.js +298 -142
- package/client/transports/README.md +50 -0
- package/client/transports/files.md +41 -0
- package/client/transports/streamParser.js +195 -0
- package/client/transports/streaming.js +555 -202
- package/dist/ape.js +6 -1
- package/dist/ape.js.map +4 -4
- package/index.d.ts +38 -16
- package/package.json +32 -7
- package/server/README.md +287 -53
- package/server/adapters/README.md +28 -19
- package/server/adapters/files.md +68 -0
- package/server/adapters/firebase.js +543 -160
- package/server/adapters/index.js +362 -112
- package/server/adapters/mongo.js +530 -140
- package/server/adapters/postgres.js +534 -155
- package/server/adapters/redis.js +508 -143
- package/server/adapters/supabase.js +555 -186
- package/server/client/README.md +43 -0
- package/server/client/connection.js +586 -0
- package/server/client/files.md +40 -0
- package/server/client/index.js +342 -0
- package/server/files.md +54 -0
- package/server/index.js +332 -27
- package/server/lib/README.md +26 -0
- package/server/lib/broadcast/clients.js +219 -0
- package/server/lib/broadcast/files.md +58 -0
- package/server/lib/broadcast/index.js +57 -0
- package/server/lib/broadcast/publishProxy.js +110 -0
- package/server/lib/broadcast/pubsub.js +137 -0
- package/server/lib/broadcast/sendProxy.js +103 -0
- package/server/lib/bun.js +315 -99
- package/server/lib/fileTransfer/README.md +63 -0
- package/server/lib/fileTransfer/files.md +30 -0
- package/server/lib/fileTransfer/streaming.js +435 -0
- package/server/lib/fileTransfer.js +710 -326
- package/server/lib/files.md +111 -0
- package/server/lib/httpUtils.js +283 -0
- package/server/lib/loader.js +208 -7
- package/server/lib/longPolling/README.md +63 -0
- package/server/lib/longPolling/files.md +44 -0
- package/server/lib/longPolling/getHandler.js +365 -0
- package/server/lib/longPolling/postHandler.js +327 -0
- package/server/lib/longPolling.js +174 -221
- package/server/lib/main.js +369 -532
- package/server/lib/runtimes/README.md +42 -0
- package/server/lib/runtimes/bun.js +586 -0
- package/server/lib/runtimes/files.md +56 -0
- package/server/lib/runtimes/node.js +511 -0
- package/server/lib/wiring.js +539 -98
- package/server/lib/ws/README.md +35 -0
- package/server/lib/ws/adapters/README.md +54 -0
- package/server/lib/ws/adapters/bun.js +538 -170
- package/server/lib/ws/adapters/deno.js +623 -149
- package/server/lib/ws/adapters/files.md +42 -0
- package/server/lib/ws/files.md +74 -0
- package/server/lib/ws/frames.js +532 -154
- package/server/lib/ws/index.js +207 -10
- package/server/lib/ws/server.js +385 -92
- package/server/lib/ws/socket.js +549 -181
- package/server/lib/wsProvider.js +363 -89
- package/server/plugins/binary.js +282 -0
- package/server/security/README.md +92 -0
- package/server/security/auth/README.md +319 -0
- package/server/security/auth/adapters/files.md +95 -0
- package/server/security/auth/adapters/ldap/constants.js +37 -0
- package/server/security/auth/adapters/ldap/files.md +19 -0
- package/server/security/auth/adapters/ldap/helpers.js +111 -0
- package/server/security/auth/adapters/ldap.js +353 -0
- package/server/security/auth/adapters/oauth2/constants.js +41 -0
- package/server/security/auth/adapters/oauth2/files.md +19 -0
- package/server/security/auth/adapters/oauth2/helpers.js +123 -0
- package/server/security/auth/adapters/oauth2.js +273 -0
- package/server/security/auth/adapters/opaque-handlers.js +314 -0
- package/server/security/auth/adapters/opaque.js +205 -0
- package/server/security/auth/adapters/saml/constants.js +52 -0
- package/server/security/auth/adapters/saml/files.md +19 -0
- package/server/security/auth/adapters/saml/helpers.js +74 -0
- package/server/security/auth/adapters/saml.js +173 -0
- package/server/security/auth/adapters/totp.js +703 -0
- package/server/security/auth/adapters/webauthn.js +625 -0
- package/server/security/auth/files.md +61 -0
- package/server/security/auth/framework/constants.js +27 -0
- package/server/security/auth/framework/files.md +23 -0
- package/server/security/auth/framework/handlers.js +272 -0
- package/server/security/auth/framework/socket-auth.js +177 -0
- package/server/security/auth/handlers/auth-messages.js +143 -0
- package/server/security/auth/handlers/files.md +28 -0
- package/server/security/auth/index.js +290 -0
- package/server/security/auth/mfa/crypto/aead.js +148 -0
- package/server/security/auth/mfa/crypto/constants.js +35 -0
- package/server/security/auth/mfa/crypto/files.md +27 -0
- package/server/security/auth/mfa/crypto/kdf.js +120 -0
- package/server/security/auth/mfa/crypto/utils.js +68 -0
- package/server/security/auth/mfa/crypto-utils.js +80 -0
- package/server/security/auth/mfa/files.md +77 -0
- package/server/security/auth/mfa/ledger/constants.js +75 -0
- package/server/security/auth/mfa/ledger/errors.js +73 -0
- package/server/security/auth/mfa/ledger/files.md +23 -0
- package/server/security/auth/mfa/ledger/share-record.js +32 -0
- package/server/security/auth/mfa/ledger.js +255 -0
- package/server/security/auth/mfa/recovery/constants.js +67 -0
- package/server/security/auth/mfa/recovery/files.md +19 -0
- package/server/security/auth/mfa/recovery/handlers.js +216 -0
- package/server/security/auth/mfa/recovery.js +191 -0
- package/server/security/auth/mfa/sss/constants.js +21 -0
- package/server/security/auth/mfa/sss/files.md +23 -0
- package/server/security/auth/mfa/sss/gf256.js +103 -0
- package/server/security/auth/mfa/sss/serialization.js +82 -0
- package/server/security/auth/mfa/sss.js +161 -0
- package/server/security/auth/mfa/two-of-three/constants.js +58 -0
- package/server/security/auth/mfa/two-of-three/files.md +23 -0
- package/server/security/auth/mfa/two-of-three/handlers.js +241 -0
- package/server/security/auth/mfa/two-of-three/helpers.js +71 -0
- package/server/security/auth/mfa/two-of-three.js +136 -0
- package/server/security/auth/nonce-manager.js +89 -0
- package/server/security/auth/state-machine-mfa.js +269 -0
- package/server/security/auth/state-machine.js +257 -0
- package/server/security/extractRootDomain.js +144 -16
- package/server/security/files.md +51 -0
- package/server/security/origin.js +197 -15
- package/server/security/reply.js +274 -16
- package/server/socket/README.md +119 -0
- package/server/socket/authMiddleware.js +299 -0
- package/server/socket/files.md +86 -0
- package/server/socket/open.js +154 -8
- package/server/socket/pluginHooks.js +334 -0
- package/server/socket/receive.js +184 -225
- package/server/socket/receiveContext.js +117 -0
- package/server/socket/send.js +416 -78
- package/server/socket/tagUtils.js +402 -0
- package/server/utils/README.md +19 -0
- package/server/utils/deepRequire.js +255 -30
- package/server/utils/files.md +57 -0
- package/server/utils/genId.js +182 -20
- package/server/utils/parseUserAgent.js +313 -251
- package/server/utils/userAgent/README.md +65 -0
- package/server/utils/userAgent/files.md +46 -0
- package/server/utils/userAgent/patterns.js +545 -0
- package/utils/README.md +21 -0
- package/utils/files.md +66 -0
- package/utils/jss/README.md +21 -0
- package/utils/jss/decode.js +471 -0
- package/utils/jss/encode.js +312 -0
- package/utils/jss/files.md +68 -0
- package/utils/jss/plugins.js +210 -0
- package/utils/jss.js +219 -273
- package/utils/messageHash.js +238 -35
- package/dist/api-ape.min.js +0 -2
- package/dist/api-ape.min.js.map +0 -7
- package/server/client.js +0 -308
- 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
|