api-ape 2.3.0 → 3.0.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 CHANGED
@@ -3,6 +3,7 @@
3
3
  [![npm version](https://img.shields.io/npm/v/api-ape.svg)](https://www.npmjs.com/package/api-ape)
4
4
  [![GitHub issues](https://img.shields.io/github/issues/codemeasandwich/api-ape)](https://github.com/codemeasandwich/api-ape/issues)
5
5
  [![Zero Dependencies](https://img.shields.io/badge/dependencies-0-brightgreen.svg)](#zero-dependencies)
6
+ [![Dependabot](https://img.shields.io/dependabot/codemeasandwich/api-ape)](https://github.com/codemeasandwich/api-ape/security/dependabot)
6
7
  [![CSRF protected](https://img.shields.io/badge/CSRF%20🚷-protected-green.svg)](#csrf-protection)
7
8
  [![Bundle Size](https://img.shields.io/bundlephobia/minzip/api-ape)](https://bundlephobia.com/package/api-ape)
8
9
  [![JJS Encoding](https://img.shields.io/badge/encoding-JJS-blue.svg)](#jjs-encoding)
@@ -32,7 +33,7 @@ yarn add api-ape
32
33
 
33
34
  ```js
34
35
  const { createServer } = require('http')
35
- const ape = require('api-ape')
36
+ const { ape } = require('api-ape')
36
37
 
37
38
  const server = createServer()
38
39
 
@@ -45,7 +46,7 @@ server.listen(3000)
45
46
  **With Express:**
46
47
  ```js
47
48
  const express = require('express')
48
- const ape = require('api-ape')
49
+ const { ape } = require('api-ape')
49
50
 
50
51
  const app = express()
51
52
  const server = app.listen(3000) // Get the HTTP server
@@ -344,7 +345,7 @@ module.exports = function(announcement) {
344
345
  ### Using ape.clients
345
346
 
346
347
  ```js
347
- const ape = require('api-ape')
348
+ const { ape } = require('api-ape')
348
349
 
349
350
  // Get online count
350
351
  console.log('Online:', ape.clients.size)
package/index.d.ts CHANGED
@@ -219,6 +219,10 @@ export interface ForestOptions {
219
219
 
220
220
  /**
221
221
  * Initialize api-ape on a Node.js HTTP/HTTPS server
222
+ *
223
+ * V3 Breaking Change:
224
+ * Old: const ape = require('api-ape')
225
+ * New: const { ape } = require('api-ape')
222
226
  */
223
227
  declare function ape(server: HttpServer, options: ApeServerOptions): void
224
228
 
@@ -280,8 +284,48 @@ declare namespace ape {
280
284
  export function message<T = any, R = any>(data?: T): Promise<R>
281
285
  }
282
286
 
283
- // Server-side default export (also works as browser client proxy)
284
- export default ape
287
+ // =============================================================================
288
+ // SERVER-SIDE CLIENT (api - same interface as browser)
289
+ // =============================================================================
290
+
291
+ /**
292
+ * Server-side client for connecting to other api-ape servers
293
+ * 100% identical interface to the browser client
294
+ *
295
+ * @example
296
+ * import api from 'api-ape'
297
+ *
298
+ * // Configure connection (or set APE_SERVER env)
299
+ * api.connect('ws://other-server:3000/api/ape')
300
+ *
301
+ * // Same usage as browser
302
+ * const result = await api.hello('World')
303
+ * api.on('message', (data) => console.log(data))
304
+ */
305
+ export interface ApeServerClient extends ApeSender {
306
+ /** Subscribe to broadcasts from the remote server */
307
+ on<T = any>(type: string, handler: MessageHandler<T>): void
308
+ on(handler: MessageHandler): void
309
+ /** Subscribe to connection state changes */
310
+ onConnectionChange(handler: (state: ConnectionState) => void): () => void
311
+ /** Connect to a server (or set APE_SERVER env) */
312
+ connect(url: string): void
313
+ /** Close the connection */
314
+ close(): void
315
+ /** Current transport type (read-only) */
316
+ readonly transport: 'websocket' | null
317
+ }
318
+
319
+ /**
320
+ * The api client - works identically on browser and server
321
+ */
322
+ declare const api: ApeServerClient
323
+
324
+ // Named export for V3 (Server usage: const { ape } = require('api-ape'))
325
+ export { ape, api }
326
+
327
+ // Default export: api client (same interface on browser and server)
328
+ export default api
285
329
 
286
330
  // =============================================================================
287
331
  // BROWSER CLIENT (Default export in browser context)
package/index.js CHANGED
@@ -1,11 +1,26 @@
1
+ /**
2
+ * api-ape unified entry point
3
+ *
4
+ * V3 Server Usage:
5
+ * const api = require('api-ape') // Client factory (default)
6
+ * const { ape } = require('api-ape') // Server initializer (named)
7
+ * import api, { ape } from 'api-ape' // ESM both
8
+ *
9
+ * Browser Usage:
10
+ * import api from 'api-ape' // Auto-connecting client
11
+ */
1
12
 
2
13
  let apiApe;
3
14
 
4
15
  if ('undefined' === typeof window
5
16
  || 'undefined' === typeof window.document) {
17
+ // Server environment - exports: api (default), ape, broadcast, clients, createClient
6
18
  apiApe = require('./server');
7
19
  } else {
20
+ // Browser environment - client module has its own exports
8
21
  apiApe = require('./client');
9
22
  }
10
23
 
11
24
  module.exports = apiApe
25
+
26
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "api-ape",
3
- "version": "2.3.0",
3
+ "version": "3.0.0",
4
4
  "description": "Remote Procedure Events (RPE) - A lightweight WebSocket framework for building real-time APIs. Call server functions from the browser like local methods with automatic reconnection, HTTP streaming fallback, and extended JSON encoding.",
5
5
  "main": "index.js",
6
6
  "browser": "./client/index.js",
package/server/README.md CHANGED
@@ -49,7 +49,7 @@ npm i api-ape
49
49
 
50
50
  ```js
51
51
  const { createServer } = require('http')
52
- const ape = require('api-ape')
52
+ const { ape } = require('api-ape')
53
53
 
54
54
  const server = createServer()
55
55
 
@@ -64,6 +64,32 @@ ape(server, {
64
64
  server.listen(3000)
65
65
  ```
66
66
 
67
+ ## Server-to-Server Connection
68
+
69
+ Your server can connect to **another** api-ape server as a client. The API is 100% identical to browser usage:
70
+
71
+ ```js
72
+ import api, { ape } from 'api-ape'
73
+
74
+ // Start your own server
75
+ ape(server, { where: 'api' })
76
+
77
+ // Connect to another api-ape server
78
+ api.connect('ws://other-server:3000/api/ape')
79
+
80
+ // Now use it exactly like browser code!
81
+ const result = await api.hello('World')
82
+ api.on('message', ({ data }) => console.log(data))
83
+ ```
84
+
85
+ **Or set the connection URL via environment variable:**
86
+
87
+ ```bash
88
+ APE_SERVER=ws://other-server:3000/api/ape node app.js
89
+ ```
90
+
91
+ This enables server-side microservice patterns while keeping the familiar api-ape interface.
92
+
67
93
  ## API
68
94
 
69
95
  ### `ape(server, options)`
@@ -273,7 +299,7 @@ The built-in polyfill implements:
273
299
  ### Quick Start
274
300
 
275
301
  ```js
276
- const ape = require('api-ape');
302
+ const { ape } = require('api-ape');
277
303
  const { createClient } = require('redis');
278
304
 
279
305
  const redis = createClient();
@@ -453,7 +479,7 @@ ape.joinVia({
453
479
 
454
480
  **Server A (port 3001):**
455
481
  ```js
456
- const ape = require('api-ape');
482
+ const { ape } = require('api-ape');
457
483
  const redis = createClient();
458
484
  await redis.connect();
459
485
 
@@ -465,7 +491,7 @@ server.listen(3001);
465
491
 
466
492
  **Server B (port 3002):**
467
493
  ```js
468
- const ape = require('api-ape');
494
+ const { ape } = require('api-ape');
469
495
  const redis = createClient();
470
496
  await redis.connect();
471
497
 
@@ -0,0 +1,299 @@
1
+ /**
2
+ * api-ape Node.js client
3
+ *
4
+ * Mirrors the browser client API exactly - same usage on server and browser.
5
+ *
6
+ * Usage (identical to browser):
7
+ * import api from 'api-ape'
8
+ *
9
+ * api.message({ user: 'Bob', text: 'Hello!' })
10
+ * api.on('message', (data) => console.log(data))
11
+ * api.onConnectionChange((state) => console.log(state))
12
+ *
13
+ * Configuration:
14
+ * Set APE_SERVER environment variable to the WebSocket URL:
15
+ * APE_SERVER=ws://other-server:3000/api/ape node app.js
16
+ *
17
+ * Or call api.connect(url) before first use
18
+ */
19
+
20
+ const jss = require('../utils/jss')
21
+ const { WebSocket: WsPolyfill } = require('./lib/ws')
22
+
23
+ // Use native WebSocket if available (Node 22+), otherwise use polyfill
24
+ const WebSocket = globalThis.WebSocket || WsPolyfill
25
+
26
+ // Connection state enum
27
+ const ConnectionState = {
28
+ Disconnected: 'disconnected',
29
+ Connecting: 'connecting',
30
+ Connected: 'connected',
31
+ Closing: 'closing'
32
+ }
33
+
34
+ // Shared state (mirrors browser client)
35
+ let ws = null
36
+ let connectionState = ConnectionState.Disconnected
37
+ const connectionChangeListeners = []
38
+ const waitingOn = {}
39
+ const receiverArray = []
40
+ const ofTypesOb = {}
41
+ let queryCounter = 0
42
+ let bufferedCalls = []
43
+ let bufferedReceivers = []
44
+ let ready = false
45
+ let reconnectEnabled = true
46
+ let reconnectTimer = null
47
+ let serverUrl = process.env.APE_SERVER || null
48
+
49
+ const joinKey = '/'
50
+ const connectTimeout = 5000
51
+ const totalRequestTimeout = 10000
52
+
53
+ function notifyConnectionChange(newState) {
54
+ if (connectionState !== newState) {
55
+ connectionState = newState
56
+ connectionChangeListeners.forEach(fn => fn(newState))
57
+ }
58
+ }
59
+
60
+ function generateQueryId() {
61
+ return `q${Date.now().toString(36)}_${(queryCounter++).toString(36)}`
62
+ }
63
+
64
+ function connect(url) {
65
+ if (url) serverUrl = url
66
+
67
+ if (!serverUrl) {
68
+ console.warn('🦍 api-ape: No server URL configured. Set APE_SERVER env or call api.connect(url)')
69
+ return
70
+ }
71
+
72
+ if (ws && ws.readyState !== WebSocket.CLOSED) {
73
+ return
74
+ }
75
+
76
+ notifyConnectionChange(ConnectionState.Connecting)
77
+
78
+ ws = new WebSocket(serverUrl)
79
+
80
+ ws.onopen = () => {
81
+ ready = true
82
+ notifyConnectionChange(ConnectionState.Connected)
83
+
84
+ // Flush buffered receivers
85
+ bufferedReceivers.forEach(({ type, handler }) => {
86
+ setOnReceiver(type, handler)
87
+ })
88
+ bufferedReceivers = []
89
+
90
+ // Flush buffered calls
91
+ bufferedCalls.forEach(({ type, data, resolve, reject, createdAt, timer }) => {
92
+ clearTimeout(timer)
93
+ send(type, data, createdAt).then(resolve).catch(reject)
94
+ })
95
+ bufferedCalls = []
96
+ }
97
+
98
+ ws.onmessage = (event) => {
99
+ const msg = jss.parse(typeof event.data === 'string' ? event.data : event.data.toString())
100
+ const { err, type, queryId, data } = msg
101
+
102
+ // Response to a query
103
+ if (queryId && waitingOn[queryId]) {
104
+ waitingOn[queryId](err, data)
105
+ delete waitingOn[queryId]
106
+ return
107
+ }
108
+
109
+ // Broadcast message
110
+ if (ofTypesOb[type]) {
111
+ ofTypesOb[type].forEach(handler => handler({ err, type, data }))
112
+ }
113
+ receiverArray.forEach(handler => handler({ err, type, data }))
114
+ }
115
+
116
+ ws.onerror = (err) => {
117
+ console.error('🦍 api-ape client error:', err.message || err)
118
+ }
119
+
120
+ ws.onclose = () => {
121
+ ready = false
122
+ ws = null
123
+ notifyConnectionChange(ConnectionState.Disconnected)
124
+
125
+ if (reconnectEnabled && serverUrl) {
126
+ reconnectTimer = setTimeout(() => connect(), 1000)
127
+ }
128
+ }
129
+ }
130
+
131
+ function send(type, data, createdAt = Date.now()) {
132
+ const queryId = generateQueryId()
133
+
134
+ return new Promise((resolve, reject) => {
135
+ const timer = setTimeout(() => {
136
+ delete waitingOn[queryId]
137
+ reject(new Error(`Request timeout: ${type}`))
138
+ }, totalRequestTimeout)
139
+
140
+ waitingOn[queryId] = (err, result) => {
141
+ clearTimeout(timer)
142
+ if (err) {
143
+ reject(typeof err === 'string' ? new Error(err) : err)
144
+ } else {
145
+ resolve(result)
146
+ }
147
+ }
148
+
149
+ const message = jss.stringify({ type, data, queryId, createdAt })
150
+ ws.send(message)
151
+ })
152
+ }
153
+
154
+ function queueOrSend(type, data) {
155
+ if (ready && ws && ws.readyState === WebSocket.OPEN) {
156
+ return send(type, data)
157
+ }
158
+
159
+ // Queue the message
160
+ return new Promise((resolve, reject) => {
161
+ const createdAt = Date.now()
162
+ const timer = setTimeout(() => {
163
+ const idx = bufferedCalls.findIndex(m => m.createdAt === createdAt)
164
+ if (idx > -1) bufferedCalls.splice(idx, 1)
165
+ reject(new Error(`Connection timeout: ${type}`))
166
+ }, connectTimeout)
167
+
168
+ bufferedCalls.push({ type, data, resolve, reject, createdAt, timer })
169
+
170
+ // Ensure we're connecting
171
+ if (connectionState === ConnectionState.Disconnected && serverUrl) {
172
+ connect()
173
+ }
174
+ })
175
+ }
176
+
177
+ /**
178
+ * Subscribe to broadcasts from the server (same as browser api.on)
179
+ */
180
+ function on(type, handler) {
181
+ if (typeof type === 'function') {
182
+ handler = type
183
+ type = null
184
+ }
185
+
186
+ if (ready) {
187
+ setOnReceiver(type, handler)
188
+ } else {
189
+ bufferedReceivers.push({ type, handler })
190
+ if (serverUrl) connect()
191
+ }
192
+ }
193
+
194
+ function setOnReceiver(type, handler) {
195
+ if (type === null) {
196
+ receiverArray.push(handler)
197
+ } else {
198
+ if (!ofTypesOb[type]) ofTypesOb[type] = []
199
+ ofTypesOb[type].push(handler)
200
+ }
201
+ }
202
+
203
+ /**
204
+ * Subscribe to connection state changes (same as browser api.onConnectionChange)
205
+ */
206
+ function onConnectionChange(handler) {
207
+ connectionChangeListeners.push(handler)
208
+ handler(connectionState)
209
+ return () => {
210
+ const idx = connectionChangeListeners.indexOf(handler)
211
+ if (idx > -1) connectionChangeListeners.splice(idx, 1)
212
+ }
213
+ }
214
+
215
+ /**
216
+ * Create the sender proxy (mirrors browser client exactly)
217
+ */
218
+ const handler = {
219
+ get(target, prop) {
220
+ // Reserved properties - same as browser
221
+ if (prop === 'on') return on
222
+ if (prop === 'onConnectionChange') return onConnectionChange
223
+ if (prop === 'transport') return ready ? 'websocket' : null
224
+ if (prop === 'connect') return connect
225
+ if (prop === 'close') return close
226
+ if (prop === 'then' || prop === 'catch') return undefined // Not a Promise
227
+
228
+ // Return a function that either calls directly or buffers
229
+ const wrapperFn = function (a, b) {
230
+ let path = joinKey + prop, body
231
+ if (arguments.length === 2) {
232
+ path += a
233
+ body = b
234
+ } else {
235
+ body = a
236
+ }
237
+ return queueOrSend(path, body)
238
+ }
239
+ return new Proxy(wrapperFn, handler)
240
+ }
241
+ }
242
+
243
+ function close() {
244
+ reconnectEnabled = false
245
+ if (reconnectTimer) {
246
+ clearTimeout(reconnectTimer)
247
+ reconnectTimer = null
248
+ }
249
+ if (ws) {
250
+ notifyConnectionChange(ConnectionState.Closing)
251
+ ws.close()
252
+ }
253
+ }
254
+
255
+ // Create the proxy (same interface as browser senderProxy)
256
+ const api = new Proxy({}, handler)
257
+
258
+ // Define properties on the proxy (same as browser)
259
+ Object.defineProperty(api, 'on', {
260
+ value: on,
261
+ writable: false,
262
+ enumerable: false,
263
+ configurable: false
264
+ })
265
+
266
+ Object.defineProperty(api, 'onConnectionChange', {
267
+ value: onConnectionChange,
268
+ writable: false,
269
+ enumerable: false,
270
+ configurable: false
271
+ })
272
+
273
+ Object.defineProperty(api, 'connect', {
274
+ value: connect,
275
+ writable: false,
276
+ enumerable: false,
277
+ configurable: false
278
+ })
279
+
280
+ Object.defineProperty(api, 'close', {
281
+ value: close,
282
+ writable: false,
283
+ enumerable: false,
284
+ configurable: false
285
+ })
286
+
287
+ // Auto-connect if APE_SERVER is set
288
+ if (serverUrl) {
289
+ connect()
290
+ }
291
+
292
+ // Export the same interface as browser
293
+ module.exports = api
294
+ module.exports.default = api
295
+ module.exports.on = on
296
+ module.exports.onConnectionChange = onConnectionChange
297
+ module.exports.connect = connect
298
+ module.exports.close = close
299
+ module.exports.ConnectionState = ConnectionState
package/server/index.js CHANGED
@@ -1,14 +1,37 @@
1
1
  /**
2
2
  * api-ape server entry point
3
- * Exports the main ape function and broadcast utilities
3
+ *
4
+ * V3 Usage (100% identical on browser and server):
5
+ * import api from 'api-ape'
6
+ * api.hello('World') // Works same on browser AND server
7
+ * api.on('message', (data) => console.log(data))
8
+ *
9
+ * Server Setup:
10
+ * import api, { ape } from 'api-ape'
11
+ * ape(server, { where: 'api' }) // Start your server
12
+ *
13
+ * // Connect to another server (set APE_SERVER env or call api.connect)
14
+ * api.connect('ws://other-server:3000/api/ape')
15
+ * api.hello('World')
16
+ *
17
+ * Supports both CommonJS and ES Modules
4
18
  */
5
19
 
6
20
  const ape = require('./lib/main')
7
21
  const { broadcast, clients } = require('./lib/broadcast')
22
+ const api = require('./client')
8
23
 
9
- // Attach broadcast utilities to the main function for clean exports
24
+ // Attach broadcast utilities to the ape function
10
25
  ape.broadcast = broadcast
11
26
  ape.clients = clients
12
27
 
13
- module.exports = ape
28
+ // Default export: api client (same interface as browser)
29
+ module.exports = api
30
+
31
+ // Named exports
32
+ module.exports.ape = ape
33
+ module.exports.api = api
34
+
35
+
36
+
14
37