api-ape 1.1.0 → 2.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.
@@ -0,0 +1,170 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * 🦍 api-ape Vue Chat Example
4
+ *
5
+ * This component demonstrates how to use api-ape in a Vue application:
6
+ *
7
+ * 1. **Client Initialization**: Connect to api-ape WebSocket server
8
+ * 2. **Proxy Pattern**: Use `client.sender` as a Proxy to call server functions
9
+ * 3. **Event Listeners**: Listen for server broadcasts using `setOnReciver`
10
+ * 4. **Promise-based Calls**: Server functions return Promises automatically
11
+ */
12
+
13
+ import { ref, onMounted } from 'vue'
14
+ import { getApeClient } from '../ape/client'
15
+ import Info from './components/Info.vue'
16
+
17
+ interface Message {
18
+ user: string
19
+ text: string
20
+ time: string
21
+ }
22
+
23
+ // Component state
24
+ const messages = ref<Message[]>([])
25
+ const input = ref('')
26
+ const username = ref('')
27
+ const joined = ref(false)
28
+ const userCount = ref(0)
29
+ const sending = ref(false)
30
+ const connectionState = ref<'disconnected' | 'connecting' | 'connected'>('connecting')
31
+
32
+ // Store the api-ape sender Proxy
33
+ let api: any = null
34
+
35
+ onMounted(async () => {
36
+ const client = await getApeClient()
37
+ if (!client) return
38
+
39
+ // Store the sender Proxy
40
+ api = client.sender
41
+ console.log('🦍 api-ape client connected')
42
+
43
+ // Subscribe to connection state changes
44
+ client.onConnectionChange((state: string) => {
45
+ connectionState.value = state as any
46
+ })
47
+
48
+ // Set up event listeners for server broadcasts
49
+ client.setOnReciver('init', ({ data }: { data: { history: Message[], users: number } }) => {
50
+ messages.value = data.history || []
51
+ userCount.value = data.users || 0
52
+ console.log('🦍 Initialized with', data.history?.length || 0, 'messages')
53
+ })
54
+
55
+ client.setOnReciver('message', ({ data }: { data: { message: Message } }) => {
56
+ messages.value.push(data.message)
57
+ })
58
+
59
+ client.setOnReciver('users', ({ data }: { data: { count: number } }) => {
60
+ userCount.value = data.count
61
+ })
62
+ })
63
+
64
+ async function sendMessage() {
65
+ if (!input.value.trim() || !api || sending.value) return
66
+
67
+ sending.value = true
68
+
69
+ try {
70
+ const response = await api.message({ user: username.value, text: input.value })
71
+ if (response?.message) {
72
+ messages.value.push(response.message)
73
+ }
74
+ } catch (err) {
75
+ console.error('Send failed:', err)
76
+ } finally {
77
+ sending.value = false
78
+ }
79
+
80
+ input.value = ''
81
+ }
82
+
83
+ function handleJoin() {
84
+ if (username.value.trim()) {
85
+ joined.value = true
86
+ }
87
+ }
88
+
89
+ function formatTime(time: string) {
90
+ return new Date(time).toLocaleTimeString()
91
+ }
92
+
93
+ function getConnectionStatus() {
94
+ if (connectionState.value === 'connected') {
95
+ if (userCount.value === 1) return '✅ Connected • Only You are online'
96
+ if (userCount.value > 1) return `✅ Connected • You + ${userCount.value - 1} are online`
97
+ return '✅ Connected'
98
+ }
99
+ if (connectionState.value === 'connecting') return '⏳ Connecting...'
100
+ return '❌ Disconnected'
101
+ }
102
+ </script>
103
+
104
+ <template>
105
+ <div class="container">
106
+ <main class="main">
107
+ <h1 class="title">
108
+ 🦍 <span class="gradient">api-ape</span> Chat
109
+ </h1>
110
+ <p class="subtitle">{{ getConnectionStatus() }}</p>
111
+
112
+ <!-- Join Form -->
113
+ <form v-if="!joined" @submit.prevent="handleJoin" class="join-form">
114
+ <input
115
+ type="text"
116
+ placeholder="Enter your name..."
117
+ v-model="username"
118
+ class="input"
119
+ autofocus
120
+ />
121
+ <button
122
+ type="submit"
123
+ class="button"
124
+ :disabled="connectionState !== 'connected'"
125
+ >
126
+ {{ connectionState === 'connected' ? 'Join Chat →' : 'Connecting...' }}
127
+ </button>
128
+ </form>
129
+
130
+ <!-- Chat Interface -->
131
+ <div v-else class="chat-container">
132
+ <div class="header">
133
+ <span>💬 {{ username }}</span>
134
+ <span class="user-count">🟢 {{ userCount }} online</span>
135
+ </div>
136
+
137
+ <div class="messages">
138
+ <p v-if="messages.length === 0" class="empty-state">
139
+ No messages yet. Say hi! 👋
140
+ </p>
141
+ <div
142
+ v-for="(msg, i) in messages"
143
+ :key="i"
144
+ :class="['message', msg.user === username ? 'my-message' : '']"
145
+ >
146
+ <strong class="username">{{ msg.user }}</strong>
147
+ <span>{{ msg.text }}</span>
148
+ <span class="time">{{ formatTime(msg.time) }}</span>
149
+ </div>
150
+ </div>
151
+
152
+ <form @submit.prevent="sendMessage" class="input-form">
153
+ <input
154
+ type="text"
155
+ placeholder="Type a message..."
156
+ v-model="input"
157
+ class="message-input"
158
+ :disabled="sending"
159
+ autofocus
160
+ />
161
+ <button type="submit" class="send-button" :disabled="sending">
162
+ {{ sending ? '...' : 'Send' }}
163
+ </button>
164
+ </form>
165
+ </div>
166
+
167
+ <Info />
168
+ </main>
169
+ </div>
170
+ </template>
@@ -0,0 +1,352 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * Info component - explains how api-ape works
4
+ */
5
+ </script>
6
+
7
+ <template>
8
+ <div class="code-section">
9
+ <h3 class="code-title">📚 How api-ape Works</h3>
10
+
11
+ <div class="grid-container">
12
+ <div class="grid-layout">
13
+ <!-- Top Left: Key Concepts -->
14
+ <div>
15
+ <h4 class="section-heading">💡 Key Concepts</h4>
16
+ <pre class="code">• 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</pre>
22
+ </div>
23
+
24
+ <!-- Top Right: Data Flow -->
25
+ <div>
26
+ <h4 class="section-heading-large">🔄 Data Flow</h4>
27
+ <div class="data-flow-grid">
28
+ <!-- Column Headers -->
29
+ <div class="column-header-client">Client</div>
30
+ <div class="grid-cell"></div>
31
+ <div class="column-header-server">Server</div>
32
+
33
+ <!-- Step 1: Client sends -->
34
+ <div class="client-box-span3">api.message(data)</div>
35
+ <div class="arrow-container-row2">
36
+ <div class="arrow-line-send"></div>
37
+ <span class="arrow-label-blue">Send</span>
38
+ <div class="arrow-head-right"></div>
39
+ </div>
40
+ <div class="empty-grid-cell"></div>
41
+
42
+ <!-- Step 2: Server receives -->
43
+ <div class="empty-grid-cell-row3"></div>
44
+ <div class="arrow-container-row3">
45
+ <div class="arrow-head-left"></div>
46
+ <span class="arrow-label-green">Return</span>
47
+ <div class="arrow-line-return"></div>
48
+ </div>
49
+ <div class="server-box-span2">api/message.js</div>
50
+
51
+ <!-- Step 3: Server broadcasts -->
52
+ <div class="empty-grid-cell-row4"></div>
53
+ <div class="arrow-container-row4">
54
+ <div class="arrow-line-broadcast"></div>
55
+ <span class="arrow-label-green">Broadcast</span>
56
+ <div class="arrow-head-right"></div>
57
+ </div>
58
+ <div class="server-box-span3">Broadcast to others</div>
59
+
60
+ <!-- Step 4: Other clients receive -->
61
+ <div class="client-box-single">Other clients</div>
62
+ <div class="arrow-container-row5">
63
+ <div class="arrow-head-left-blue"></div>
64
+ <span class="arrow-label-blue">Broadcast</span>
65
+ <div class="arrow-line-broadcast-return"></div>
66
+ </div>
67
+ <div class="empty-grid-cell-row5"></div>
68
+ </div>
69
+ </div>
70
+
71
+ <!-- Bottom Left: Client-Side -->
72
+ <div>
73
+ <h4 class="section-heading">🔵 Client-Side (Browser)</h4>
74
+ <pre class="code">// 1. Initialize api-ape client
75
+ const client = await getApeClient()
76
+ const api = client.sender // Proxy object
77
+
78
+ // 2. Call server function - property name = file path
79
+ // api.message() → calls api/message.js
80
+ api.message({ user: 'Alice', text: 'Hello!' })
81
+ .then(response => {
82
+ // Server returned: { ok: true, message: {...} }
83
+ console.log('Response:', response)
84
+ })
85
+ .catch(err => {
86
+ // Server threw an error
87
+ console.error('Error:', err)
88
+ })
89
+
90
+ // 3. Listen for server broadcasts
91
+ client.setOnReciver('message', ({ data }) => {
92
+ // Server called: this.broadcastOthers('message', data)
93
+ // This fires for ALL clients except the sender
94
+ console.log('Broadcast received:', data.message)
95
+ })</pre>
96
+ </div>
97
+
98
+ <!-- Bottom Right: Server-Side -->
99
+ <div>
100
+ <h4 class="section-heading">🟢 Server-Side (api/message.js)</h4>
101
+ <pre class="code">// File: api/message.js
102
+ // This function is called when client does: api.message(data)
103
+
104
+ module.exports = function message(data) {
105
+ const { user, text } = data
106
+
107
+ // Validate input
108
+ if (!user || !text) {
109
+ throw new Error('Missing user or text')
110
+ }
111
+
112
+ const msg = {
113
+ user,
114
+ text,
115
+ time: new Date().toISOString()
116
+ }
117
+
118
+ // Broadcast to ALL OTHER clients (not the sender)
119
+ this.broadcastOthers('message', { message: msg })
120
+
121
+ // Return response to sender (fulfills Promise)
122
+ return { ok: true, message: msg }
123
+ }</pre>
124
+ </div>
125
+ </div>
126
+ </div>
127
+ </div>
128
+ </template>
129
+
130
+ <style scoped>
131
+ .code-section {
132
+ margin-top: 2rem;
133
+ }
134
+
135
+ .code-title {
136
+ margin-bottom: 0.5rem;
137
+ color: #0f0;
138
+ }
139
+
140
+ .grid-container {
141
+ max-width: 1200px;
142
+ margin: 1.5rem auto 0;
143
+ padding: 0 1rem;
144
+ width: 100%;
145
+ box-sizing: border-box;
146
+ }
147
+
148
+ .grid-layout {
149
+ display: grid;
150
+ grid-template-columns: 1fr;
151
+ gap: 2rem;
152
+ width: 100%;
153
+ }
154
+
155
+ @media (min-width: 768px) {
156
+ .grid-layout {
157
+ grid-template-columns: 1fr 1fr;
158
+ }
159
+ }
160
+
161
+ .section-heading {
162
+ margin-bottom: 0.5rem;
163
+ font-size: 0.9rem;
164
+ font-weight: bold;
165
+ }
166
+
167
+ .section-heading-large {
168
+ margin-bottom: 1rem;
169
+ font-size: 0.9rem;
170
+ font-weight: bold;
171
+ }
172
+
173
+ .code {
174
+ background: rgba(0, 0, 0, 0.4);
175
+ padding: 1.5rem;
176
+ border-radius: 12px;
177
+ font-size: 0.75rem;
178
+ color: #0f0;
179
+ overflow: auto;
180
+ white-space: pre-wrap;
181
+ font-family: monospace;
182
+ }
183
+
184
+ .data-flow-grid {
185
+ display: grid;
186
+ grid-template-columns: 200px 1fr 200px;
187
+ grid-template-rows: auto auto auto auto auto;
188
+ gap: 1rem;
189
+ align-items: stretch;
190
+ }
191
+
192
+ .column-header-client {
193
+ font-size: 0.8rem;
194
+ font-weight: bold;
195
+ text-align: center;
196
+ grid-row: 1;
197
+ grid-column: 1;
198
+ color: #00d2ff;
199
+ }
200
+
201
+ .column-header-server {
202
+ font-size: 0.8rem;
203
+ font-weight: bold;
204
+ text-align: center;
205
+ grid-row: 1;
206
+ grid-column: 3;
207
+ color: #00e676;
208
+ }
209
+
210
+ .client-box-span3 {
211
+ background: linear-gradient(135deg, #3a7bd5, #00d2ff);
212
+ padding: 0.75rem 1rem;
213
+ border-radius: 8px;
214
+ color: #fff;
215
+ font-size: 0.75rem;
216
+ font-weight: bold;
217
+ box-shadow: 0 4px 12px rgba(58, 123, 213, 0.4);
218
+ display: flex;
219
+ align-items: center;
220
+ justify-content: center;
221
+ grid-column: 1;
222
+ grid-row: 2 / 5;
223
+ }
224
+
225
+ .client-box-single {
226
+ background: linear-gradient(135deg, #3a7bd5, #00d2ff);
227
+ padding: 0.75rem 1rem;
228
+ border-radius: 8px;
229
+ color: #fff;
230
+ font-size: 0.75rem;
231
+ font-weight: bold;
232
+ box-shadow: 0 4px 12px rgba(58, 123, 213, 0.4);
233
+ display: flex;
234
+ align-items: center;
235
+ justify-content: center;
236
+ grid-column: 1;
237
+ grid-row: 5;
238
+ }
239
+
240
+ .server-box-span2 {
241
+ background: linear-gradient(135deg, #00c851, #00e676);
242
+ padding: 0.75rem 1rem;
243
+ border-radius: 8px;
244
+ color: #fff;
245
+ font-size: 0.75rem;
246
+ font-weight: bold;
247
+ box-shadow: 0 4px 12px rgba(0, 200, 81, 0.4);
248
+ display: flex;
249
+ align-items: center;
250
+ justify-content: center;
251
+ grid-column: 3;
252
+ grid-row: 2 / 4;
253
+ }
254
+
255
+ .server-box-span3 {
256
+ background: linear-gradient(135deg, #00c851, #00e676);
257
+ padding: 0.75rem 1rem;
258
+ border-radius: 8px;
259
+ color: #fff;
260
+ font-size: 0.75rem;
261
+ font-weight: bold;
262
+ box-shadow: 0 4px 12px rgba(0, 200, 81, 0.4);
263
+ display: flex;
264
+ align-items: center;
265
+ justify-content: center;
266
+ grid-column: 3;
267
+ grid-row: 4 / 6;
268
+ }
269
+
270
+ .arrow-container-row2,
271
+ .arrow-container-row3,
272
+ .arrow-container-row4,
273
+ .arrow-container-row5 {
274
+ display: flex;
275
+ align-items: center;
276
+ justify-content: center;
277
+ gap: 0.5rem;
278
+ grid-column: 2;
279
+ }
280
+
281
+ .arrow-container-row2 { grid-row: 2; }
282
+ .arrow-container-row3 { grid-row: 3; }
283
+ .arrow-container-row4 { grid-row: 4; }
284
+ .arrow-container-row5 { grid-row: 5; }
285
+
286
+ .arrow-line-send {
287
+ flex: 1;
288
+ height: 2px;
289
+ background: linear-gradient(90deg, #00d2ff, #00e676);
290
+ }
291
+
292
+ .arrow-line-return {
293
+ flex: 1;
294
+ height: 2px;
295
+ background: linear-gradient(90deg, #00e676, transparent);
296
+ }
297
+
298
+ .arrow-line-broadcast {
299
+ flex: 1;
300
+ height: 2px;
301
+ background: linear-gradient(90deg, transparent, #00e676);
302
+ }
303
+
304
+ .arrow-line-broadcast-return {
305
+ flex: 1;
306
+ height: 2px;
307
+ background: linear-gradient(90deg, #00d2ff, transparent);
308
+ }
309
+
310
+ .arrow-label-blue {
311
+ font-size: 0.7rem;
312
+ white-space: nowrap;
313
+ padding: 0 0.5rem;
314
+ color: #00d2ff;
315
+ }
316
+
317
+ .arrow-label-green {
318
+ font-size: 0.7rem;
319
+ white-space: nowrap;
320
+ padding: 0 0.5rem;
321
+ color: #00e676;
322
+ }
323
+
324
+ .arrow-head-right {
325
+ width: 0;
326
+ height: 0;
327
+ border-top: 4px solid transparent;
328
+ border-bottom: 4px solid transparent;
329
+ border-left: 8px solid #00e676;
330
+ }
331
+
332
+ .arrow-head-left {
333
+ width: 0;
334
+ height: 0;
335
+ border-top: 4px solid transparent;
336
+ border-bottom: 4px solid transparent;
337
+ border-right: 8px solid #00e676;
338
+ }
339
+
340
+ .arrow-head-left-blue {
341
+ width: 0;
342
+ height: 0;
343
+ border-top: 4px solid transparent;
344
+ border-bottom: 4px solid transparent;
345
+ border-right: 8px solid #00d2ff;
346
+ }
347
+
348
+ .empty-grid-cell { grid-row: 2; grid-column: 3; }
349
+ .empty-grid-cell-row3 { grid-row: 3; grid-column: 1; }
350
+ .empty-grid-cell-row4 { grid-row: 4; grid-column: 1; }
351
+ .empty-grid-cell-row5 { grid-row: 5; grid-column: 3; }
352
+ </style>
@@ -0,0 +1,5 @@
1
+ import { createApp } from 'vue'
2
+ import App from './App.vue'
3
+ import './style.css'
4
+
5
+ createApp(App).mount('#app')