api-ape 0.0.0 → 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.
Files changed (64) hide show
  1. package/README.md +458 -0
  2. package/client/README.md +69 -0
  3. package/client/browser.js +17 -0
  4. package/client/connectSocket.js +260 -0
  5. package/dist/ape.js +454 -0
  6. package/example/ExpressJs/README.md +97 -0
  7. package/example/ExpressJs/api/message.js +11 -0
  8. package/example/ExpressJs/backend.js +37 -0
  9. package/example/ExpressJs/index.html +88 -0
  10. package/example/ExpressJs/package-lock.json +834 -0
  11. package/example/ExpressJs/package.json +10 -0
  12. package/example/ExpressJs/styles.css +128 -0
  13. package/example/NextJs/.dockerignore +29 -0
  14. package/example/NextJs/Dockerfile +52 -0
  15. package/example/NextJs/Dockerfile.dev +27 -0
  16. package/example/NextJs/README.md +113 -0
  17. package/example/NextJs/ape/client.js +66 -0
  18. package/example/NextJs/ape/embed.js +12 -0
  19. package/example/NextJs/ape/index.js +23 -0
  20. package/example/NextJs/ape/logic/chat.js +62 -0
  21. package/example/NextJs/ape/onConnect.js +69 -0
  22. package/example/NextJs/ape/onDisconnect.js +13 -0
  23. package/example/NextJs/ape/onError.js +9 -0
  24. package/example/NextJs/ape/onReceive.js +15 -0
  25. package/example/NextJs/ape/onSend.js +15 -0
  26. package/example/NextJs/api/message.js +44 -0
  27. package/example/NextJs/docker-compose.yml +22 -0
  28. package/example/NextJs/next-env.d.ts +5 -0
  29. package/example/NextJs/next.config.js +8 -0
  30. package/example/NextJs/package-lock.json +5107 -0
  31. package/example/NextJs/package.json +25 -0
  32. package/example/NextJs/pages/Info.tsx +153 -0
  33. package/example/NextJs/pages/_app.tsx +6 -0
  34. package/example/NextJs/pages/index.tsx +264 -0
  35. package/example/NextJs/public/favicon.ico +0 -0
  36. package/example/NextJs/public/vercel.svg +4 -0
  37. package/example/NextJs/server.js +40 -0
  38. package/example/NextJs/styles/Chat.module.css +448 -0
  39. package/example/NextJs/styles/Home.module.css +129 -0
  40. package/example/NextJs/styles/globals.css +26 -0
  41. package/example/NextJs/tsconfig.json +20 -0
  42. package/example/README.md +66 -0
  43. package/index.d.ts +179 -0
  44. package/index.js +11 -0
  45. package/package.json +11 -4
  46. package/server/README.md +93 -0
  47. package/server/index.js +6 -0
  48. package/server/lib/broadcast.js +63 -0
  49. package/server/lib/loader.js +10 -0
  50. package/server/lib/main.js +23 -0
  51. package/server/lib/wiring.js +94 -0
  52. package/server/security/extractRootDomain.js +21 -0
  53. package/server/security/origin.js +13 -0
  54. package/server/security/reply.js +21 -0
  55. package/server/socket/open.js +10 -0
  56. package/server/socket/receive.js +66 -0
  57. package/server/socket/send.js +55 -0
  58. package/server/utils/deepRequire.js +45 -0
  59. package/server/utils/genId.js +24 -0
  60. package/todo.md +85 -0
  61. package/utils/jss.js +273 -0
  62. package/utils/jss.test.js +261 -0
  63. package/utils/messageHash.js +43 -0
  64. package/utils/messageHash.test.js +56 -0
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "apiape_example",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "node server.js",
7
+ "build": "next build",
8
+ "start": "node server.js",
9
+ "lint": "next lint"
10
+ },
11
+ "dependencies": {
12
+ "@types/node": "18.11.13",
13
+ "@types/react": "18.0.26",
14
+ "@types/react-dom": "18.0.9",
15
+ "api-ape": "file:../..",
16
+ "eslint": "8.29.0",
17
+ "eslint-config-next": "13.0.6",
18
+ "express": "^4.18.2",
19
+ "next": "13.0.6",
20
+ "react": "18.2.0",
21
+ "react-dom": "18.2.0",
22
+ "typescript": "4.9.4",
23
+ "ws": "^8.14.0"
24
+ }
25
+ }
@@ -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
+ }
@@ -0,0 +1,6 @@
1
+ import '../styles/globals.css'
2
+ import type { AppProps } from 'next/app'
3
+
4
+ export default function App({ Component, pageProps }: AppProps) {
5
+ return <Component {...pageProps} />
6
+ }
@@ -0,0 +1,264 @@
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
+
21
+ import Head from 'next/head'
22
+ import { useState, useEffect, useRef } from 'react'
23
+ import styles from '../styles/Chat.module.css'
24
+ import { getApeClient } from '../ape/client'
25
+ import Info from './Info'
26
+
27
+ export default function Home() {
28
+ // Component state
29
+ const [messages, setMessages] = useState([])
30
+ const [input, setInput] = useState('')
31
+ const [username, setUsername] = useState('')
32
+ const [joined, setJoined] = useState(false)
33
+ const [userCount, setUserCount] = useState(0)
34
+ const [sending, setSending] = useState(false)
35
+ const [connected, setConnected] = useState(false)
36
+
37
+ // Refs
38
+ const apiRef = useRef(null) // Stores the api-ape sender Proxy
39
+
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
+ */
50
+ useEffect(() => {
51
+ // Skip on server-side rendering
52
+ if (typeof window === 'undefined') return
53
+
54
+ getApeClient().then((client) => {
55
+ if (!client) return
56
+
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
+ */
66
+ apiRef.current = client.sender
67
+ setConnected(true)
68
+ console.log('🦍 api-ape client connected')
69
+
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
+ */
82
+ client.setOnReciver('init', ({ data }) => {
83
+ // Server sent initial data (happens on connect)
84
+ setMessages(data.history || [])
85
+ setUserCount(data.users || 0)
86
+ console.log('🦍 Initialized with', data.history?.length || 0, 'messages')
87
+ })
88
+
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!
92
+ setMessages(prev => [...prev, data.message])
93
+ })
94
+
95
+ client.setOnReciver('users', ({ data }) => {
96
+ // Server broadcasted updated user count
97
+ setUserCount(data.count)
98
+ })
99
+ })
100
+ }, [])
101
+
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
+ */
118
+ const sendMessage = (e) => {
119
+ e.preventDefault()
120
+ if (!input.trim() || !apiRef.current || sending) return
121
+
122
+ const api = apiRef.current
123
+ setSending(true)
124
+
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
+ */
140
+ api.message({ user: username, text: input })
141
+ .then((response) => {
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
+ */
151
+ if (response?.message) {
152
+ setMessages(prev => [...prev, response.message])
153
+ }
154
+ setSending(false)
155
+ })
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
+ */
163
+ console.error('Send failed:', err)
164
+ setSending(false)
165
+ })
166
+
167
+ setInput('')
168
+ }
169
+
170
+ /**
171
+ * Handle user joining the chat
172
+ * Simply sets the joined state to show the chat interface
173
+ */
174
+ const handleJoin = (e) => {
175
+ e.preventDefault()
176
+ if (username.trim()) {
177
+ setJoined(true)
178
+ }
179
+ }
180
+
181
+ return (
182
+ <div className={styles.container}>
183
+ <Head>
184
+ <title>🦍 api-ape Chat</title>
185
+ <meta name="description" content="Real-time WebSocket chat using api-ape" />
186
+ </Head>
187
+
188
+ <main className={styles.main}>
189
+ <h1 className={styles.title}>
190
+ 🦍 <span className={styles.gradient}>api-ape</span> Chat
191
+ </h1>
192
+ <p className={styles.subtitle}>
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...'}
200
+ </p>
201
+
202
+ {!joined ? (
203
+ <form onSubmit={handleJoin} className={styles.joinForm}>
204
+ <input
205
+ type="text"
206
+ placeholder="Enter your name..."
207
+ value={username}
208
+ onChange={(e) => setUsername(e.target.value)}
209
+ className={styles.input}
210
+ autoFocus
211
+ />
212
+ <button type="submit" className={styles.button} disabled={!connected}>
213
+ {connected ? 'Join Chat →' : 'Connecting...'}
214
+ </button>
215
+ </form>
216
+ ) : (
217
+ <div className={styles.chatContainer}>
218
+ <div className={styles.header}>
219
+ <span>💬 {username}</span>
220
+ <span className={styles.userCount}>
221
+ 🟢 {userCount} online
222
+ </span>
223
+ </div>
224
+
225
+ <div className={styles.messages}>
226
+ {messages.length === 0 && (
227
+ <p className={styles.emptyState}>No messages yet. Say hi! 👋</p>
228
+ )}
229
+ {messages.map((msg, i) => (
230
+ <div
231
+ key={i}
232
+ className={`${styles.message} ${msg.user === username ? styles.myMessage : ''}`}
233
+ >
234
+ <strong className={styles.username}>{msg.user}</strong>
235
+ <span>{msg.text}</span>
236
+ <span className={styles.time}>
237
+ {new Date(msg.time).toLocaleTimeString()}
238
+ </span>
239
+ </div>
240
+ ))}
241
+ </div>
242
+
243
+ <form onSubmit={sendMessage} className={styles.inputForm}>
244
+ <input
245
+ type="text"
246
+ placeholder="Type a message..."
247
+ value={input}
248
+ onChange={(e) => setInput(e.target.value)}
249
+ className={styles.messageInput}
250
+ disabled={sending}
251
+ autoFocus
252
+ />
253
+ <button type="submit" className={styles.sendButton} disabled={sending}>
254
+ {sending ? '...' : 'Send'}
255
+ </button>
256
+ </form>
257
+ </div>
258
+ )}
259
+
260
+ <Info />
261
+ </main>
262
+ </div>
263
+ )
264
+ }
@@ -0,0 +1,4 @@
1
+ <svg width="283" height="64" viewBox="0 0 283 64" fill="none"
2
+ xmlns="http://www.w3.org/2000/svg">
3
+ <path d="M141.04 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.46 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM248.72 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.45 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM200.24 34c0 6 3.92 10 10 10 4.12 0 7.21-1.87 8.8-4.92l7.68 4.43c-3.18 5.3-9.14 8.49-16.48 8.49-11.05 0-19-7.2-19-18s7.96-18 19-18c7.34 0 13.29 3.19 16.48 8.49l-7.68 4.43c-1.59-3.05-4.68-4.92-8.8-4.92-6.07 0-10 4-10 10zm82.48-29v46h-9V5h9zM36.95 0L73.9 64H0L36.95 0zm92.38 5l-27.71 48L73.91 5H84.3l17.32 30 17.32-30h10.39zm58.91 12v9.69c-1-.29-2.06-.49-3.2-.49-5.81 0-10 4-10 10V51h-9V17h9v9.2c0-5.08 5.91-9.2 13.2-9.2z" fill="#000"/>
4
+ </svg>
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Custom Next.js server using actual api-ape library with Express
3
+ */
4
+
5
+ const express = require('express')
6
+ const next = require('next')
7
+ const ape = require('api-ape')
8
+ const { onConnect } = require('./ape/onConnect')
9
+
10
+ const dev = process.env.NODE_ENV !== 'production'
11
+ const hostname = '0.0.0.0'
12
+ const port = parseInt(process.env.PORT, 10) || 3000
13
+
14
+ const app = next({ dev, hostname, port })
15
+ const handle = app.getRequestHandler()
16
+
17
+ app.prepare().then(() => {
18
+ const server = express()
19
+
20
+ // Initialize api-ape - it handles EVERYTHING
21
+ // Developer never touches WebSocket!
22
+ ape(server, { where: 'api', onConnent: onConnect })
23
+
24
+ // Let Next.js handle all other routes
25
+ server.all('*', (req, res) => {
26
+ return handle(req, res)
27
+ })
28
+
29
+ server.listen(port, () => {
30
+ console.log(`
31
+ ╔═══════════════════════════════════════════════════════╗
32
+ ║ 🦍 api-ape NextJS Demo ║
33
+ ╠═══════════════════════════════════════════════════════╣
34
+ ║ HTTP: http://localhost:${port}/ ║
35
+ ║ WebSocket: ws://localhost:${port}/api/ape ║
36
+ ║ ape(app, { where: "api", onConnent }) ║
37
+ ╚═══════════════════════════════════════════════════════╝
38
+ `)
39
+ })
40
+ })