api-ape 1.0.1 → 1.0.2

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
@@ -1,53 +1,25 @@
1
1
  # 🦍 api-ape
2
2
 
3
- **Remote Procedure Events (RPE)** — A lightweight WebSocket framework for real-time APIs.
3
+ [![npm version](https://img.shields.io/npm/v/api-ape.svg)](https://www.npmjs.com/package/api-ape)
4
+ [![license](https://img.shields.io/npm/l/api-ape.svg)](https://github.com/codemeasandwich/api-ape/blob/main/LICENSE)
5
+ [![GitHub issues](https://img.shields.io/github/issues/codemeasandwich/api-ape)](https://github.com/codemeasandwich/api-ape/issues)
4
6
 
5
- Call server functions from the browser like local methods. Get real-time broadcasts with zero setup.
6
-
7
- ```js
8
- // Client: call server function, get result
9
- const pets = await ape.pets.list()
10
-
11
- // Client: listen for broadcasts
12
- ape.on('newPet', ({ data }) => console.log('New pet:', data))
13
- ```
14
-
15
- file: api/pets/list.js
16
- ```js
17
- // Server: define function, broadcast to others
18
- module.exports = function list() {
19
- return getPetList()
20
- }
21
- ```
22
-
23
- file: api/pets/newPet.js
24
- ```js
25
- // Server: define function, broadcast to others
26
- module.exports = function newPet(data) {
27
- // broadcast to all other clients
28
- this.broadcastOthers('newPet', data)
29
- return savePet(data)
30
- }
31
- ```
7
+ **Remote Procedure Events (RPE)** — A lightweight WebSocket framework for building real-time APIs. Call server functions from the browser like local methods. Get real-time broadcasts with zero setup.
32
8
 
33
9
  ---
34
10
 
35
- ## Features
36
-
37
- - **🔌 Auto-wiring** — Drop JS files in a folder, they become API endpoints
38
- - **📡 Real-time** — Built-in broadcast to all or other clients
39
- - **🔄 Reconnection** — Client auto-reconnects on disconnect
40
- - **📦 JJS Encoding** — Supports Date, RegExp, Error, Set, Map, undefined over the wire
41
- - **🎯 Simple API** — Promise-based calls with chainable paths
42
-
43
- ---
44
-
45
- ## Installation
11
+ ## Install
46
12
 
47
13
  ```bash
48
14
  npm install api-ape
15
+ # or
16
+ pnpm add api-ape
17
+ # or
18
+ yarn add api-ape
49
19
  ```
50
20
 
21
+ **Requirements:** Node.js 14+ (for server), modern browsers (for client)
22
+
51
23
  ---
52
24
 
53
25
  ## Quick Start
@@ -66,9 +38,9 @@ ape(app, { where: 'api' })
66
38
  app.listen(3000)
67
39
  ```
68
40
 
69
- ### Controllers
41
+ ### Create a Controller
70
42
 
71
- Create files in your `api/` folder. Each export becomes an endpoint:
43
+ Drop a file in your `api/` folder it automatically becomes an endpoint:
72
44
 
73
45
  ```js
74
46
  // api/hello.js
@@ -77,24 +49,16 @@ module.exports = function(name) {
77
49
  }
78
50
  ```
79
51
 
80
- ```js
81
- // api/message.js
82
- module.exports = function(data) {
83
- // Broadcast to all OTHER connected clients
84
- this.broadcastOthers('message', data)
85
- return data
86
- }
87
- ```
88
-
89
52
  ### Client (Browser)
90
53
 
91
- Include the bundled client:
54
+ Include the bundled client and start calling:
92
55
 
93
56
  ```html
94
57
  <script src="/api/ape.js"></script>
95
58
  <script>
96
- // Call server functions
97
- ape.hello('World').then(result => console.log(result)) // "Hello, World!"
59
+ // Call server functions like local methods
60
+ const result = await ape.hello('World')
61
+ console.log(result) // "Hello, World!"
98
62
 
99
63
  // Listen for broadcasts
100
64
  ape.on('message', ({ data }) => {
@@ -103,6 +67,19 @@ Include the bundled client:
103
67
  </script>
104
68
  ```
105
69
 
70
+ **That's it!** Your server function is now callable from the browser.
71
+
72
+ ---
73
+
74
+ ## Key Concepts
75
+
76
+ * **Auto-wiring** — Drop JS files in a folder, they become API endpoints automatically
77
+ * **Real-time broadcasts** — Built-in `broadcast()` and `broadcastOthers()` methods for pushing to clients
78
+ * **Promise-based calls** — Chainable paths like `ape.users.list()` map to `api/users/list.js`
79
+ * **Automatic reconnection** — Client auto-reconnects on disconnect with exponential backoff
80
+ * **JJS Encoding** — Extended JSON supporting Date, RegExp, Error, Set, Map, undefined, and circular refs
81
+ * **Connection lifecycle hooks** — Customize behavior on connect, receive, send, error, and disconnect
82
+
106
83
  ---
107
84
 
108
85
  ## API Reference
@@ -115,8 +92,8 @@ Initialize api-ape on an Express app.
115
92
 
116
93
  | Option | Type | Description |
117
94
  |--------|------|-------------|
118
- | `where` | `string` | Directory containing controller files |
119
- | `onConnent` | `function` | Connection lifecycle hook (see below) |
95
+ | `where` | `string` | Directory containing controller files (default: `'api'`) |
96
+ | `onConnent` | `function` | Connection lifecycle hook (see [Connection Lifecycle](#connection-lifecycle)) |
120
97
 
121
98
  #### Controller Context (`this`)
122
99
 
@@ -124,8 +101,8 @@ Inside controller functions, `this` provides:
124
101
 
125
102
  | Property | Description |
126
103
  |----------|-------------|
127
- | `this.broadcast(type, data)` | Send to ALL connected clients |
128
- | `this.broadcastOthers(type, data)` | Send to all EXCEPT the caller |
104
+ | `this.broadcast(type, data)` | Send to **ALL** connected clients |
105
+ | `this.broadcastOthers(type, data)` | Send to all **EXCEPT** the caller |
129
106
  | `this.online()` | Get count of connected clients |
130
107
  | `this.getClients()` | Get array of connected hostIds |
131
108
  | `this.hostId` | Unique ID of the calling client |
@@ -133,17 +110,61 @@ Inside controller functions, `this` provides:
133
110
  | `this.socket` | WebSocket instance |
134
111
  | `this.agent` | Parsed user-agent (browser, OS, device) |
135
112
 
136
- #### Connection Lifecycle
113
+ ### Client
114
+
115
+ #### `ape.<path>.<method>(...args)`
116
+
117
+ Call a server function. Returns a Promise.
118
+
119
+ ```js
120
+ // Calls api/users/list.js
121
+ const users = await ape.users.list()
122
+
123
+ // Calls api/users/create.js with data
124
+ const user = await ape.users.create({ name: 'Alice' })
125
+
126
+ // Nested paths work too
127
+ // ape.admin.users -> api/admin/users.js
128
+ // ape.admin.users.delete -> api/admin/users/delete.js
129
+ await ape.admin.users.delete(userId)
130
+ ```
131
+
132
+ #### `ape.on(type, handler)`
133
+
134
+ Listen for server broadcasts.
135
+
136
+ ```js
137
+ ape.on('notification', ({ data, err, type }) => {
138
+ console.log('Received:', data)
139
+ })
140
+ ```
141
+
142
+ ---
143
+
144
+ ## Configuration
145
+
146
+ ### Default Options
147
+
148
+ ```js
149
+ ape(app, {
150
+ where: 'api', // Controller directory
151
+ onConnent: undefined // Lifecycle hook (optional)
152
+ })
153
+ ```
154
+
155
+ ### Connection Lifecycle Hook
156
+
157
+ Customize behavior per connection:
137
158
 
138
159
  ```js
139
160
  ape(app, {
140
161
  where: 'api',
141
- onConnent(socket, req, send) {
162
+ onConnent(socket, req, hostId) {
142
163
  return {
143
164
  // Embed values into `this` for all controllers
144
165
  embed: {
145
166
  userId: req.session?.userId,
146
- clientId: send + '' // hostId as string
167
+ clientId: String(hostId)
147
168
  },
148
169
 
149
170
  // Before/after hooks
@@ -164,31 +185,81 @@ ape(app, {
164
185
  })
165
186
  ```
166
187
 
167
- ### Client
188
+ ---
168
189
 
169
- #### `ape.<path>.<method>(data)`
190
+ ## Common Recipes
170
191
 
171
- Call a server function. Returns a Promise.
192
+ ### Broadcast to Other Clients
172
193
 
173
194
  ```js
174
- // Calls api/users/list.js
175
- ape.users.list().then(users => ...)
195
+ // api/message.js
196
+ module.exports = function(data) {
197
+ // Broadcast to all OTHER connected clients (not the sender)
198
+ this.broadcastOthers('message', data)
199
+ return { success: true }
200
+ }
201
+ ```
176
202
 
177
- // Calls api/users/create.js with data
178
- ape.users.create({ name: 'Alice' }).then(user => ...)
203
+ ### Broadcast to All Clients
179
204
 
180
- // Nested paths work too
181
- ape.admin.users.delete(userId).then(() => ...)
205
+ ```js
206
+ // api/announcement.js
207
+ module.exports = function(announcement) {
208
+ // Broadcast to ALL connected clients including sender
209
+ this.broadcast('announcement', announcement)
210
+ return { sent: true }
211
+ }
182
212
  ```
183
213
 
184
- #### `ape.on(type, handler)`
214
+ ### Get Online Count
185
215
 
186
- Listen for server broadcasts.
216
+ ```js
217
+ // api/stats.js
218
+ module.exports = function() {
219
+ return {
220
+ online: this.online(),
221
+ clients: this.getClients()
222
+ }
223
+ }
224
+ ```
225
+
226
+ ### Access Request Data
187
227
 
188
228
  ```js
189
- ape.on('notification', ({ data, err, type }) => {
190
- console.log('Received:', data)
191
- })
229
+ // api/profile.js
230
+ module.exports = function() {
231
+ // Access original HTTP request
232
+ const userId = this.req.session?.userId
233
+ const userAgent = this.agent.browser.name
234
+
235
+ return { userId, userAgent }
236
+ }
237
+ ```
238
+
239
+ ### Error Handling
240
+
241
+ ```js
242
+ // api/data.js
243
+ module.exports = async function(id) {
244
+ try {
245
+ const data = await fetchData(id)
246
+ return data
247
+ } catch (err) {
248
+ // Errors are automatically sent to client
249
+ throw new Error(`Failed to fetch: ${err.message}`)
250
+ }
251
+ }
252
+ ```
253
+
254
+ ### Client-Side Error Handling
255
+
256
+ ```js
257
+ try {
258
+ const result = await ape.data.get(id)
259
+ console.log(result)
260
+ } catch (err) {
261
+ console.error('Server error:', err)
262
+ }
192
263
  ```
193
264
 
194
265
  ---
@@ -207,25 +278,143 @@ api-ape uses **JJS (JSON SuperSet)** encoding, which extends JSON to support:
207
278
  | `Map` | ✅ Preserved as Map |
208
279
  | Circular refs | ✅ Handled via pointers |
209
280
 
210
- This is automatic — send a Date, receive a Date.
281
+ This is automatic — send a Date, receive a Date. No configuration needed.
211
282
 
212
283
  ---
213
284
 
214
- ## Examples
285
+ ## Examples & Demos
286
+
287
+ The repository contains working examples:
215
288
 
216
- See the [`example/`](./example) folder:
289
+ * **`example/ExpressJs/`** — Simple real-time chat app
290
+ - Minimal setup with Express.js
291
+ - Broadcast messages to other clients
292
+ - Message history
217
293
 
218
- - **ExpressJs/**Simple chat app with broadcast
219
- - **NextJs/** — Integration with Next.js
294
+ * **`example/NextJs/`**Production-ready chat application
295
+ - Custom Next.js server integration
296
+ - React hooks integration
297
+ - User presence tracking
298
+ - Docker support
299
+ - Connection lifecycle hooks
220
300
 
221
- Run the Express example:
301
+ ### Run an Example
222
302
 
303
+ **ExpressJs:**
223
304
  ```bash
224
305
  cd example/ExpressJs
225
306
  npm install
226
307
  npm start
308
+ # Open http://localhost:3000
309
+ ```
310
+
311
+ **NextJs:**
312
+ ```bash
313
+ cd example/NextJs
314
+ npm install
315
+ npm run dev
316
+ # Open http://localhost:3000
317
+ ```
318
+
319
+ Or with Docker:
320
+ ```bash
321
+ cd example/NextJs
322
+ docker-compose up --build
323
+ ```
324
+
325
+ ---
326
+
327
+ ## Troubleshooting & FAQ
328
+
329
+ ### CORS Errors in Browser
330
+
331
+ Ensure your Express server allows WebSocket connections from your origin. api-ape uses `express-ws` which handles CORS automatically, but verify your Express CORS middleware allows WebSocket upgrade requests.
332
+
333
+ ### Controller Not Found
334
+
335
+ * Check that your controller file is in the `where` directory (default: `api/`)
336
+ * Ensure the file exports a function: `module.exports = function(...) { ... }`
337
+ * File paths map directly: `api/users/list.js` → `ape.users.list()`
338
+
339
+ ### Connection Drops Frequently
340
+
341
+ The client automatically reconnects with exponential backoff. If connections drop often:
342
+ * Check server WebSocket timeout settings
343
+ * Verify network stability
344
+ * Check server logs for errors
345
+
346
+ ### Binary Data / File Uploads
347
+
348
+ JJS encoding supports complex types, but for large binary data, consider:
349
+ * Sending file URLs instead of raw data
350
+ * Using a separate file upload endpoint
351
+ * Chunking large payloads
352
+
353
+ ### TypeScript Support
354
+
355
+ Type definitions are included (`index.d.ts`). For full type safety, you may need to:
356
+ * Define interfaces for your controller parameters and return types
357
+ * Use type assertions when calling `ape.<path>.<method>()`
358
+
359
+ ---
360
+
361
+ ## Tests & CI
362
+
363
+ ```bash
364
+ npm test # Run test suite
365
+ npm run test:watch # Watch mode
366
+ npm run test:cover # Coverage report
227
367
  ```
228
368
 
369
+ **Test Commands:**
370
+ - `npm test` — Run all tests
371
+ - `npm run test:watch` — Watch mode for development
372
+ - `npm run test:cover` — Generate coverage report
373
+ - `npm run test:update` — Update snapshots
374
+
375
+ **Supported:** Node.js 14+, modern browsers (Chrome, Firefox, Safari, Edge)
376
+
377
+ ---
378
+
379
+ ## Contributing
380
+
381
+ Contributions welcome! Here's how to help:
382
+
383
+ 1. **Fork the repository**
384
+ 2. **Create a branch:** `git checkout -b feature/your-feature-name`
385
+ 3. **Make your changes** and add tests
386
+ 4. **Run tests:** `npm test`
387
+ 5. **Commit:** Follow conventional commit messages
388
+ 6. **Push and open a PR** with a clear description
389
+
390
+ **Guidelines:**
391
+ * Add tests for new features
392
+ * Keep code style consistent
393
+ * Update documentation if needed
394
+ * Ensure all tests pass
395
+
396
+ ---
397
+
398
+ ## Releases / Changelog
399
+
400
+ Versioning follows [Semantic Versioning](https://semver.org/).
401
+
402
+ **Current version:** See `package.json` or npm registry
403
+
404
+ **Release notes:** Check [GitHub releases](https://github.com/codemeasandwich/api-ape/releases) for detailed changelog.
405
+
406
+ ---
407
+
408
+ ## Security
409
+
410
+ **Reporting vulnerabilities:** Please report security issues via [GitHub Security Advisories](https://github.com/codemeasandwich/api-ape/security/advisories) or email the maintainer.
411
+
412
+ **Security considerations:**
413
+ * Validate all input in controllers
414
+ * Use authentication/authorization in `onConnent` hooks
415
+ * Sanitize data before broadcasting
416
+ * Keep dependencies up to date
417
+
229
418
  ---
230
419
 
231
420
  ## Project Structure
@@ -245,7 +434,7 @@ api-ape/
245
434
  │ │ ├── receive.js # Incoming message handler
246
435
  │ │ └── send.js # Outgoing message handler
247
436
  │ └── security/
248
- │ └── reply.js # Duplicate request protection
437
+ │ └── reply.js # Duplicate request protection
249
438
  ├── utils/
250
439
  │ ├── jss.js # JSON SuperSet encoder/decoder
251
440
  │ └── messageHash.js # Request deduplication
@@ -256,6 +445,14 @@ api-ape/
256
445
 
257
446
  ---
258
447
 
259
- ## License
448
+ ## License & Authors
449
+
450
+ **License:** MIT
451
+
452
+ **Author:** [Brian Shannon](https://github.com/codemeasandwich)
453
+
454
+ **Repository:** [github.com/codemeasandwich/api-ape](https://github.com/codemeasandwich/api-ape)
455
+
456
+ ---
260
457
 
261
- MIT © [Brian Shannon](https://github.com/codemeasandwich)
458
+ **Made with 🦍 by the api-ape community**
@@ -0,0 +1,153 @@
1
+ import styles from '../styles/Chat.module.css'
2
+
3
+ export default function Info() {
4
+ return (
5
+ <div className={styles.codeSection}>
6
+ <h3 className={styles.codeTitle}>📚 How api-ape Works</h3>
7
+
8
+ <div className={styles.gridContainer}>
9
+ <div className={styles.gridLayout}>
10
+ {/* Top Left: Key Concepts */}
11
+ <div>
12
+ <h4 className={styles.sectionHeading}>
13
+ 💡 Key Concepts
14
+ </h4>
15
+ <pre className={styles.code}>
16
+ {`• Proxy Pattern: api.message() → api/message.js
17
+ • Auto-wiring: Drop files in api/ folder, they become endpoints
18
+ • Promises: All calls return Promises automatically
19
+ • Broadcasts: Use this.broadcast() or this.broadcastOthers()
20
+ • Context: this.broadcast, this.hostId, this.req available in controllers
21
+ • Auto-reconnect: Client reconnects automatically on disconnect`}
22
+ </pre>
23
+ </div>
24
+
25
+ {/* Top Right: Data Flow */}
26
+ <div>
27
+ <h4 className={styles.sectionHeadingLarge}>
28
+ 🔄 Data Flow
29
+ </h4>
30
+ <div className={styles.dataFlowGrid}>
31
+ {/* Column Headers */}
32
+ <div className={styles.columnHeaderClient}>
33
+ Client
34
+ </div>
35
+ <div className={styles.gridCell}></div>
36
+ <div className={styles.columnHeaderServer}>
37
+ Server
38
+ </div>
39
+
40
+ {/* Step 1: Client sends */}
41
+ <div className={styles.clientBoxSpan3}>
42
+ api.message(data)
43
+ </div>
44
+ <div className={styles.arrowContainerRow2}>
45
+ <div className={styles.arrowLineSend}></div>
46
+ <span className={styles.arrowLabelBlue}>Send</span>
47
+ <div className={styles.arrowHeadRight}></div>
48
+ </div>
49
+ <div className={styles.emptyGridCell}></div>
50
+
51
+ {/* Step 2: Server receives */}
52
+ <div className={styles.emptyGridCellRow3}></div>
53
+ <div className={styles.arrowContainerRow3}>
54
+ <div className={styles.arrowHeadLeft}></div>
55
+ <span className={styles.arrowLabelGreen}>Return</span>
56
+ <div className={styles.arrowLineReturn}></div>
57
+ </div>
58
+ <div className={styles.serverBoxSpan2}>
59
+ api/message.js
60
+ </div>
61
+
62
+ {/* Step 3: Server broadcasts */}
63
+ <div className={styles.emptyGridCellRow4}></div>
64
+ <div className={styles.arrowContainerRow4}>
65
+ <div className={styles.arrowLineBroadcast}></div>
66
+ <span className={styles.arrowLabelGreen}>Broadcast</span>
67
+ <div className={styles.arrowHeadRight}></div>
68
+ </div>
69
+ <div className={styles.serverBoxSpan3}>
70
+ Broadcast to others
71
+ </div>
72
+
73
+ {/* Step 4: Other clients receive */}
74
+ <div className={styles.clientBoxSingle}>
75
+ Other clients
76
+ </div>
77
+ <div className={styles.arrowContainerRow5}>
78
+ <div className={styles.arrowHeadLeftBlue}></div>
79
+ <span className={styles.arrowLabelBlue}>Broadcast</span>
80
+ <div className={styles.arrowLineBroadcastReturn}></div>
81
+ </div>
82
+ <div className={styles.emptyGridCellRow5}></div>
83
+
84
+ </div>
85
+ </div>
86
+
87
+ {/* Bottom Left: Client-Side */}
88
+ <div>
89
+ <h4 className={styles.sectionHeading}>
90
+ 🔵 Client-Side (Browser)
91
+ </h4>
92
+ <pre className={styles.code}>
93
+ {`// 1. Initialize api-ape client
94
+ const client = await getApeClient()
95
+ const api = client.sender // Proxy object
96
+
97
+ // 2. Call server function - property name = file path
98
+ // api.message() → calls api/message.js
99
+ api.message({ user: 'Alice', text: 'Hello!' })
100
+ .then(response => {
101
+ // Server returned: { ok: true, message: {...} }
102
+ console.log('Response:', response)
103
+ })
104
+ .catch(err => {
105
+ // Server threw an error
106
+ console.error('Error:', err)
107
+ })
108
+
109
+ // 3. Listen for server broadcasts
110
+ client.setOnReciver('message', ({ data }) => {
111
+ // Server called: this.broadcastOthers('message', data)
112
+ // This fires for ALL clients except the sender
113
+ console.log('Broadcast received:', data.message)
114
+ })`}
115
+ </pre>
116
+ </div>
117
+
118
+ {/* Bottom Right: Server-Side */}
119
+ <div>
120
+ <h4 className={styles.sectionHeading}>
121
+ 🟢 Server-Side (api/message.js)
122
+ </h4>
123
+ <pre className={styles.code}>
124
+ {`// File: api/message.js
125
+ // This function is called when client does: api.message(data)
126
+
127
+ module.exports = function message(data) {
128
+ const { user, text } = data
129
+
130
+ // Validate input
131
+ if (!user || !text) {
132
+ throw new Error('Missing user or text')
133
+ }
134
+
135
+ const msg = {
136
+ user,
137
+ text,
138
+ time: new Date().toISOString()
139
+ }
140
+
141
+ // Broadcast to ALL OTHER clients (not the sender)
142
+ this.broadcastOthers('message', { message: msg })
143
+
144
+ // Return response to sender (fulfills Promise)
145
+ return { ok: true, message: msg }
146
+ }`}
147
+ </pre>
148
+ </div>
149
+ </div>
150
+ </div>
151
+ </div>
152
+ )
153
+ }
@@ -1,9 +1,31 @@
1
+ /**
2
+ * 🦍 api-ape Next.js Chat Example
3
+ *
4
+ * This component demonstrates how to use api-ape in a React/Next.js application:
5
+ *
6
+ * 1. **Client Initialization**: Connect to api-ape WebSocket server
7
+ * 2. **Proxy Pattern**: Use `client.sender` as a Proxy to call server functions
8
+ * 3. **Event Listeners**: Listen for server broadcasts using `setOnReciver`
9
+ * 4. **Promise-based Calls**: Server functions return Promises automatically
10
+ *
11
+ * Server-side: api/message.js handles incoming messages and broadcasts to other clients
12
+ * Client-side: This component sends messages and receives broadcasts
13
+ *
14
+ * Key api-ape concepts:
15
+ * - `client.sender` is a Proxy - accessing `sender.message()` calls server function
16
+ * - Property name (`message`) maps to server file: `api/message.js`
17
+ * - `setOnReciver(type, handler)` listens for server broadcasts
18
+ * - All calls return Promises - server response is automatically matched by queryId
19
+ */
20
+
1
21
  import Head from 'next/head'
2
22
  import { useState, useEffect, useRef } from 'react'
3
23
  import styles from '../styles/Chat.module.css'
4
24
  import { getApeClient } from '../ape/client'
25
+ import Info from './Info'
5
26
 
6
27
  export default function Home() {
28
+ // Component state
7
29
  const [messages, setMessages] = useState([])
8
30
  const [input, setInput] = useState('')
9
31
  const [username, setUsername] = useState('')
@@ -11,67 +33,133 @@ export default function Home() {
11
33
  const [userCount, setUserCount] = useState(0)
12
34
  const [sending, setSending] = useState(false)
13
35
  const [connected, setConnected] = useState(false)
14
- const messagesEndRef = useRef(null)
15
- const apiRef = useRef(null) // The sender proxy
36
+
37
+ // Refs
38
+ const apiRef = useRef(null) // Stores the api-ape sender Proxy
16
39
 
17
- // Initialize api-ape client on mount (before join)
40
+ /**
41
+ * Initialize api-ape client on component mount
42
+ *
43
+ * This effect:
44
+ * 1. Gets the api-ape client singleton (connects to WebSocket)
45
+ * 2. Stores the `sender` Proxy in a ref for later use
46
+ * 3. Sets up event listeners for server broadcasts
47
+ *
48
+ * The client auto-reconnects if the connection drops.
49
+ */
18
50
  useEffect(() => {
51
+ // Skip on server-side rendering
19
52
  if (typeof window === 'undefined') return
20
53
 
21
54
  getApeClient().then((client) => {
22
55
  if (!client) return
23
56
 
24
- // Store the sender proxy - ready to use!
57
+ /**
58
+ * Store the sender Proxy
59
+ *
60
+ * `client.sender` is a Proxy object that allows you to call server functions
61
+ * by accessing properties. For example:
62
+ * - `sender.message(data)` calls `api/message.js` on the server
63
+ * - The property name (`message`) maps to the server file path
64
+ * - All calls return Promises that resolve with the server's response
65
+ */
25
66
  apiRef.current = client.sender
26
67
  setConnected(true)
27
- console.log('🦍 api-ape ready')
68
+ console.log('🦍 api-ape client connected')
28
69
 
29
- // Set up message listeners
70
+ /**
71
+ * Set up event listeners for server broadcasts
72
+ *
73
+ * `setOnReciver(type, handler)` listens for broadcasts from the server.
74
+ * The server can broadcast using `this.broadcast()` or `this.broadcastOthers()`
75
+ * in controller functions (see api/message.js).
76
+ *
77
+ * Broadcast types:
78
+ * - 'init': Initial data when client connects (history, user count)
79
+ * - 'message': New message from another client
80
+ * - 'users': Updated user count
81
+ */
30
82
  client.setOnReciver('init', ({ data }) => {
83
+ // Server sent initial data (happens on connect)
31
84
  setMessages(data.history || [])
32
85
  setUserCount(data.users || 0)
33
- console.log('🦍 Initialized')
86
+ console.log('🦍 Initialized with', data.history?.length || 0, 'messages')
34
87
  })
35
88
 
36
89
  client.setOnReciver('message', ({ data }) => {
90
+ // Server broadcasted a new message from another client
91
+ // This is NOT the response to our own send - it's a broadcast!
37
92
  setMessages(prev => [...prev, data.message])
38
93
  })
39
94
 
40
95
  client.setOnReciver('users', ({ data }) => {
96
+ // Server broadcasted updated user count
41
97
  setUserCount(data.count)
42
98
  })
43
99
  })
44
100
  }, [])
45
101
 
46
- // Auto-scroll to bottom
47
- useEffect(() => {
48
- messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
49
- }, [messages])
50
-
51
- // Send message using the proxy pattern
52
- // api.message(data) -> sends type="message" with data
53
- // Returns a Promise - server can reply with matching queryId
102
+ /**
103
+ * Send a message to the server
104
+ *
105
+ * This demonstrates the api-ape Proxy pattern:
106
+ *
107
+ * 1. Access `api.message()` - the property name 'message' maps to `api/message.js`
108
+ * 2. Call it with data - returns a Promise
109
+ * 3. Server processes the request in `api/message.js`
110
+ * 4. Server can:
111
+ * - Return a value (fulfills the Promise)
112
+ * - Broadcast to other clients using `this.broadcastOthers()`
113
+ * - Throw an error (rejects the Promise)
114
+ *
115
+ * The Promise resolves with whatever the server function returns.
116
+ * The server also broadcasts to other clients (see api/message.js).
117
+ */
54
118
  const sendMessage = (e) => {
55
119
  e.preventDefault()
56
120
  if (!input.trim() || !apiRef.current || sending) return
57
121
 
58
122
  const api = apiRef.current
59
-
60
123
  setSending(true)
61
124
 
62
- // api.message({ user, text }) - "message" is the type!
63
- // createdAt is auto-added by client
64
- // queryId hash is auto-generated for request/response matching
125
+ /**
126
+ * Call server function using Proxy pattern
127
+ *
128
+ * `api.message({ user, text })`:
129
+ * - Calls the `message` function in `api/message.js`
130
+ * - Sends `{ user, text }` as the function argument
131
+ * - Returns a Promise that resolves with the server's return value
132
+ * - Server automatically broadcasts to other clients (see api/message.js)
133
+ *
134
+ * The server function receives the data and can:
135
+ * - Validate input
136
+ * - Store the message
137
+ * - Broadcast to others: `this.broadcastOthers('message', { message: msg })`
138
+ * - Return a response: `return { ok: true, message: msg }`
139
+ */
65
140
  api.message({ user: username, text: input })
66
141
  .then((response) => {
67
- // Server replied with matching queryId
68
- // Add our own message to the list (we sent it, so we add it)
142
+ /**
143
+ * Server responded successfully
144
+ *
145
+ * The response is whatever the server function returned.
146
+ * In this case, api/message.js returns: `{ ok: true, message: msg }`
147
+ *
148
+ * Note: Other clients receive the message via broadcast (setOnReciver above),
149
+ * but we add it here from the server's response to show it immediately.
150
+ */
69
151
  if (response?.message) {
70
152
  setMessages(prev => [...prev, response.message])
71
153
  }
72
154
  setSending(false)
73
155
  })
74
156
  .catch((err) => {
157
+ /**
158
+ * Server function threw an error or connection failed
159
+ *
160
+ * Errors from server functions are automatically caught and
161
+ * the Promise is rejected with the error.
162
+ */
75
163
  console.error('Send failed:', err)
76
164
  setSending(false)
77
165
  })
@@ -79,6 +167,10 @@ export default function Home() {
79
167
  setInput('')
80
168
  }
81
169
 
170
+ /**
171
+ * Handle user joining the chat
172
+ * Simply sets the joined state to show the chat interface
173
+ */
82
174
  const handleJoin = (e) => {
83
175
  e.preventDefault()
84
176
  if (username.trim()) {
@@ -98,7 +190,13 @@ export default function Home() {
98
190
  🦍 <span className={styles.gradient}>api-ape</span> Chat
99
191
  </h1>
100
192
  <p className={styles.subtitle}>
101
- {connected ? '✅ Connected' : '⏳ Connecting...'} • Pure WebSocket
193
+ {connected ? (
194
+ userCount === 1
195
+ ? '✅ Connected • Only You are online'
196
+ : userCount > 1
197
+ ? `✅ Connected • You + ${userCount - 1} are online`
198
+ : '✅ Connected'
199
+ ) : '⏳ Connecting...'}
102
200
  </p>
103
201
 
104
202
  {!joined ? (
@@ -140,7 +238,6 @@ export default function Home() {
140
238
  </span>
141
239
  </div>
142
240
  ))}
143
- <div ref={messagesEndRef} />
144
241
  </div>
145
242
 
146
243
  <form onSubmit={sendMessage} className={styles.inputForm}>
@@ -160,22 +257,7 @@ export default function Home() {
160
257
  </div>
161
258
  )}
162
259
 
163
- <div className={styles.codeSection}>
164
- <h3 className={styles.codeTitle}>✨ api-ape Proxy Pattern</h3>
165
- <pre className={styles.code}>
166
- {`// Sender is a Proxy - prop name = type
167
- const api = client.sender
168
-
169
- // Send with Promise - queryId auto-matched
170
- setSending(true)
171
- api.message({ user, text })
172
- .then(() => setSending(false))
173
- .catch(err => console.error(err))
174
-
175
- // createdAt auto-added by client
176
- // queryId hash matches request/response`}
177
- </pre>
178
- </div>
260
+ <Info />
179
261
  </main>
180
262
  </div>
181
263
  )
@@ -6,9 +6,10 @@
6
6
  }
7
7
 
8
8
  .main {
9
- max-width: 600px;
9
+ max-width: 1400px;
10
10
  margin: 0 auto;
11
11
  padding: 2rem;
12
+ width: 100%;
12
13
  }
13
14
 
14
15
  .title {
@@ -192,3 +193,256 @@
192
193
  padding: 0.2rem 0.5rem;
193
194
  border-radius: 4px;
194
195
  }
196
+
197
+ .gridContainer {
198
+ max-width: 1200px;
199
+ margin: 1.5rem auto 0;
200
+ padding: 0 1rem;
201
+ width: 100%;
202
+ box-sizing: border-box;
203
+ }
204
+
205
+ .gridLayout {
206
+ display: grid;
207
+ grid-template-columns: 1fr;
208
+ gap: 2rem;
209
+ width: 100%;
210
+ }
211
+
212
+ @media (min-width: 768px) {
213
+ .gridLayout {
214
+ grid-template-columns: 1fr 1fr;
215
+ }
216
+ }
217
+
218
+ @media (max-width: 767px) {
219
+ .gridContainer {
220
+ padding: 0 0.5rem;
221
+ }
222
+
223
+ .main {
224
+ padding: 1rem;
225
+ }
226
+ }
227
+
228
+ /* Info component styles */
229
+ .sectionHeading {
230
+ margin-bottom: 0.5rem;
231
+ font-size: 0.9rem;
232
+ font-weight: bold;
233
+ }
234
+
235
+ .sectionHeadingLarge {
236
+ margin-bottom: 1rem;
237
+ font-size: 0.9rem;
238
+ font-weight: bold;
239
+ }
240
+
241
+ .dataFlowGrid {
242
+ display: grid;
243
+ grid-template-columns: 200px 1fr 200px;
244
+ grid-template-rows: auto auto auto auto auto;
245
+ gap: 1rem;
246
+ align-items: stretch;
247
+ }
248
+
249
+ .columnHeaderClient {
250
+ font-size: 0.8rem;
251
+ font-weight: bold;
252
+ text-align: center;
253
+ grid-row: 1;
254
+ grid-column: 1;
255
+ color: #00d2ff;
256
+ }
257
+
258
+ .columnHeaderServer {
259
+ font-size: 0.8rem;
260
+ font-weight: bold;
261
+ text-align: center;
262
+ grid-row: 1;
263
+ grid-column: 3;
264
+ color: #00e676;
265
+ }
266
+
267
+ .gridCell {
268
+ grid-row: 1;
269
+ grid-column: 2;
270
+ }
271
+
272
+ .clientBoxSpan3 {
273
+ background: linear-gradient(135deg, #3a7bd5, #00d2ff);
274
+ padding: 0.75rem 1rem;
275
+ border-radius: 8px;
276
+ color: #fff;
277
+ font-size: 0.75rem;
278
+ font-weight: bold;
279
+ box-shadow: 0 4px 12px rgba(58, 123, 213, 0.4);
280
+ display: flex;
281
+ align-items: center;
282
+ justify-content: center;
283
+ grid-column: 1;
284
+ grid-row: 2 / 5;
285
+ }
286
+
287
+ .clientBoxSingle {
288
+ background: linear-gradient(135deg, #3a7bd5, #00d2ff);
289
+ padding: 0.75rem 1rem;
290
+ border-radius: 8px;
291
+ color: #fff;
292
+ font-size: 0.75rem;
293
+ font-weight: bold;
294
+ box-shadow: 0 4px 12px rgba(58, 123, 213, 0.4);
295
+ display: flex;
296
+ align-items: center;
297
+ justify-content: center;
298
+ grid-column: 1;
299
+ grid-row: 5;
300
+ }
301
+
302
+ .serverBoxSpan2 {
303
+ background: linear-gradient(135deg, #00c851, #00e676);
304
+ padding: 0.75rem 1rem;
305
+ border-radius: 8px;
306
+ color: #fff;
307
+ font-size: 0.75rem;
308
+ font-weight: bold;
309
+ box-shadow: 0 4px 12px rgba(0, 200, 81, 0.4);
310
+ display: flex;
311
+ align-items: center;
312
+ justify-content: center;
313
+ grid-column: 3;
314
+ grid-row: 2 / 4;
315
+ }
316
+
317
+ .serverBoxSpan3 {
318
+ background: linear-gradient(135deg, #00c851, #00e676);
319
+ padding: 0.75rem 1rem;
320
+ border-radius: 8px;
321
+ color: #fff;
322
+ font-size: 0.75rem;
323
+ font-weight: bold;
324
+ box-shadow: 0 4px 12px rgba(0, 200, 81, 0.4);
325
+ display: flex;
326
+ align-items: center;
327
+ justify-content: center;
328
+ grid-column: 3;
329
+ grid-row: 4 / 6;
330
+ }
331
+
332
+ .arrowContainerRow2 {
333
+ display: flex;
334
+ align-items: center;
335
+ justify-content: center;
336
+ gap: 0.5rem;
337
+ grid-column: 2;
338
+ grid-row: 2;
339
+ }
340
+
341
+ .arrowContainerRow3 {
342
+ display: flex;
343
+ align-items: center;
344
+ justify-content: center;
345
+ gap: 0.5rem;
346
+ grid-column: 2;
347
+ grid-row: 3;
348
+ }
349
+
350
+ .arrowContainerRow4 {
351
+ display: flex;
352
+ align-items: center;
353
+ justify-content: center;
354
+ gap: 0.5rem;
355
+ grid-column: 2;
356
+ grid-row: 4;
357
+ }
358
+
359
+ .arrowContainerRow5 {
360
+ display: flex;
361
+ align-items: center;
362
+ justify-content: center;
363
+ gap: 0.5rem;
364
+ grid-column: 2;
365
+ grid-row: 5;
366
+ }
367
+
368
+ .arrowLineSend {
369
+ flex: 1;
370
+ height: 2px;
371
+ background: linear-gradient(90deg, #00d2ff, #00e676);
372
+ }
373
+
374
+ .arrowLineReturn {
375
+ flex: 1;
376
+ height: 2px;
377
+ background: linear-gradient(90deg, #00e676, transparent);
378
+ }
379
+
380
+ .arrowLineBroadcast {
381
+ flex: 1;
382
+ height: 2px;
383
+ background: linear-gradient(90deg, transparent, #00e676);
384
+ }
385
+
386
+ .arrowLineBroadcastReturn {
387
+ flex: 1;
388
+ height: 2px;
389
+ background: linear-gradient(90deg, #00d2ff, transparent);
390
+ }
391
+
392
+ .arrowLabelBlue {
393
+ font-size: 0.7rem;
394
+ white-space: nowrap;
395
+ padding: 0 0.5rem;
396
+ color: #00d2ff;
397
+ }
398
+
399
+ .arrowLabelGreen {
400
+ font-size: 0.7rem;
401
+ white-space: nowrap;
402
+ padding: 0 0.5rem;
403
+ color: #00e676;
404
+ }
405
+
406
+ .arrowHeadRight {
407
+ width: 0;
408
+ height: 0;
409
+ border-top: 4px solid transparent;
410
+ border-bottom: 4px solid transparent;
411
+ border-left: 8px solid #00e676;
412
+ }
413
+
414
+ .arrowHeadLeft {
415
+ width: 0;
416
+ height: 0;
417
+ border-top: 4px solid transparent;
418
+ border-bottom: 4px solid transparent;
419
+ border-right: 8px solid #00e676;
420
+ }
421
+
422
+ .arrowHeadLeftBlue {
423
+ width: 0;
424
+ height: 0;
425
+ border-top: 4px solid transparent;
426
+ border-bottom: 4px solid transparent;
427
+ border-right: 8px solid #00d2ff;
428
+ }
429
+
430
+ .emptyGridCell {
431
+ grid-row: 2;
432
+ grid-column: 3;
433
+ }
434
+
435
+ .emptyGridCellRow3 {
436
+ grid-row: 3;
437
+ grid-column: 1;
438
+ }
439
+
440
+ .emptyGridCellRow4 {
441
+ grid-row: 4;
442
+ grid-column: 1;
443
+ }
444
+
445
+ .emptyGridCellRow5 {
446
+ grid-row: 5;
447
+ grid-column: 3;
448
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "api-ape",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "Remote procedure events",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",