api-ape 1.1.0 → 2.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.
Files changed (41) hide show
  1. package/README.md +114 -11
  2. package/client/browser.js +19 -1
  3. package/client/connectSocket.js +556 -368
  4. package/client/transports/streaming.js +253 -0
  5. package/dist/ape.js +651 -301
  6. package/example/Bun/README.md +74 -0
  7. package/example/Bun/api/message.ts +11 -0
  8. package/example/Bun/index.html +76 -0
  9. package/example/Bun/package.json +9 -0
  10. package/example/Bun/server.ts +59 -0
  11. package/example/Bun/styles.css +128 -0
  12. package/example/ExpressJs/README.md +5 -7
  13. package/example/ExpressJs/backend.js +23 -21
  14. package/example/NextJs/ape/client.js +3 -3
  15. package/example/NextJs/ape/onConnect.js +5 -5
  16. package/example/NextJs/package-lock.json +1353 -60
  17. package/example/NextJs/package.json +0 -1
  18. package/example/NextJs/pages/index.tsx +21 -10
  19. package/example/NextJs/server.js +7 -11
  20. package/example/README.md +51 -0
  21. package/example/Vite/README.md +68 -0
  22. package/example/Vite/ape/client.ts +66 -0
  23. package/example/Vite/ape/onConnect.ts +52 -0
  24. package/example/Vite/api/message.ts +57 -0
  25. package/example/Vite/index.html +16 -0
  26. package/example/Vite/package.json +19 -0
  27. package/example/Vite/server.ts +62 -0
  28. package/example/Vite/src/App.vue +170 -0
  29. package/example/Vite/src/components/Info.vue +352 -0
  30. package/example/Vite/src/main.ts +5 -0
  31. package/example/Vite/src/style.css +200 -0
  32. package/example/Vite/src/vite-env.d.ts +7 -0
  33. package/example/Vite/vite.config.ts +20 -0
  34. package/index.d.ts +40 -3
  35. package/package.json +26 -11
  36. package/server/README.md +54 -9
  37. package/server/index.js +10 -2
  38. package/server/lib/longPolling.js +221 -0
  39. package/server/lib/main.js +172 -60
  40. package/server/security/origin.js +16 -4
  41. package/server/utils/deepRequire.js +25 -10
@@ -0,0 +1,253 @@
1
+ import jss from '../../utils/jss'
2
+
3
+ /**
4
+ * HTTP Streaming transport - fallback when WebSocket is blocked
5
+ * Uses fetch + ReadableStream for receiving, POST for sending
6
+ */
7
+
8
+ // Configuration
9
+ let configuredPort = null
10
+ let configuredHost = null
11
+
12
+ /**
13
+ * Configure transport connection options
14
+ */
15
+ function configure(opts = {}) {
16
+ if (opts.port) configuredPort = opts.port
17
+ if (opts.host) configuredHost = opts.host
18
+ }
19
+
20
+ /**
21
+ * Get base URL for polling endpoints
22
+ */
23
+ function getPollUrl() {
24
+ const hostname = configuredHost || window.location.hostname
25
+ const localServers = ["localhost", "127.0.0.1", "[::1]"]
26
+ const isLocal = localServers.includes(hostname)
27
+ const isHttps = window.location.protocol === "https:"
28
+
29
+ const defaultPort = isLocal ? 9010 : (window.location.port || (isHttps ? 443 : 80))
30
+ const port = configuredPort || defaultPort
31
+
32
+ const protocol = isHttps ? "https" : "http"
33
+ const portSuffix = (isLocal || (port !== 80 && port !== 443)) ? `:${port}` : ""
34
+
35
+ return `${protocol}://${hostname}${portSuffix}/api/ape/poll`
36
+ }
37
+
38
+ /**
39
+ * Parse JSON objects from a streaming buffer by counting braces
40
+ * Handles strings containing braces correctly
41
+ */
42
+ function parseStreamBuffer(buffer) {
43
+ const messages = []
44
+ let start = -1
45
+ let depth = 0
46
+ let inString = false
47
+ let escaped = false
48
+
49
+ for (let i = 0; i < buffer.length; i++) {
50
+ const char = buffer[i]
51
+
52
+ if (escaped) {
53
+ escaped = false
54
+ continue
55
+ }
56
+
57
+ if (char === '\\' && inString) {
58
+ escaped = true
59
+ continue
60
+ }
61
+
62
+ if (char === '"') {
63
+ inString = !inString
64
+ continue
65
+ }
66
+
67
+ if (inString) continue
68
+
69
+ if (char === '{') {
70
+ if (depth === 0) {
71
+ start = i
72
+ }
73
+ depth++
74
+ } else if (char === '}') {
75
+ depth--
76
+ if (depth === 0 && start !== -1) {
77
+ const jsonStr = buffer.slice(start, i + 1)
78
+ try {
79
+ messages.push(jss.parse(jsonStr))
80
+ } catch (e) {
81
+ console.error('🦍 Failed to parse stream message:', e)
82
+ }
83
+ start = -1
84
+ }
85
+ }
86
+ }
87
+
88
+ // Return remaining buffer (incomplete message)
89
+ const remaining = start !== -1 ? buffer.slice(start) : ''
90
+ return { messages, remaining }
91
+ }
92
+
93
+ /**
94
+ * Create streaming transport instance
95
+ */
96
+ function createStreamingTransport() {
97
+ let isActive = false
98
+ let abortController = null
99
+ let streamBuffer = ''
100
+ let reconnectTimer = null
101
+
102
+ // Callbacks
103
+ let onMessage = () => { }
104
+ let onOpen = () => { }
105
+ let onClose = () => { }
106
+ let onError = () => { }
107
+
108
+ /**
109
+ * Start the streaming connection
110
+ */
111
+ async function connect() {
112
+ if (isActive) return
113
+
114
+ isActive = true
115
+ abortController = new AbortController()
116
+
117
+ try {
118
+ const response = await fetch(getPollUrl(), {
119
+ method: 'GET',
120
+ credentials: 'include',
121
+ signal: abortController.signal,
122
+ headers: {
123
+ 'Accept': 'application/json'
124
+ }
125
+ })
126
+
127
+ if (!response.ok) {
128
+ throw new Error(`Stream connect failed: ${response.status}`)
129
+ }
130
+
131
+ onOpen()
132
+
133
+ const reader = response.body.getReader()
134
+ const decoder = new TextDecoder()
135
+
136
+ async function read() {
137
+ while (isActive) {
138
+ try {
139
+ const { done, value } = await reader.read()
140
+
141
+ if (done) {
142
+ // Stream ended - reconnect
143
+ scheduleReconnect()
144
+ return
145
+ }
146
+
147
+ streamBuffer += decoder.decode(value, { stream: true })
148
+ const { messages, remaining } = parseStreamBuffer(streamBuffer)
149
+ streamBuffer = remaining
150
+
151
+ for (const msg of messages) {
152
+ // Skip heartbeat messages
153
+ if (msg.type === '__heartbeat__') continue
154
+ onMessage(msg)
155
+ }
156
+ } catch (readErr) {
157
+ if (readErr.name === 'AbortError') return
158
+ console.error('🦍 Stream read error:', readErr)
159
+ scheduleReconnect()
160
+ return
161
+ }
162
+ }
163
+ }
164
+
165
+ read()
166
+
167
+ } catch (err) {
168
+ if (err.name === 'AbortError') return
169
+
170
+ console.error('🦍 Stream connection error:', err)
171
+ onError(err)
172
+ scheduleReconnect()
173
+ }
174
+ }
175
+
176
+ /**
177
+ * Schedule reconnection with small delay
178
+ */
179
+ function scheduleReconnect() {
180
+ if (!isActive) return
181
+
182
+ if (reconnectTimer) {
183
+ clearTimeout(reconnectTimer)
184
+ }
185
+
186
+ reconnectTimer = setTimeout(() => {
187
+ if (isActive) {
188
+ connect()
189
+ }
190
+ }, 500)
191
+ }
192
+
193
+ /**
194
+ * Send a message via POST
195
+ */
196
+ async function send(type, data, createdAt) {
197
+ const payload = {
198
+ type,
199
+ data,
200
+ createdAt: new Date(createdAt)
201
+ }
202
+
203
+ const response = await fetch(getPollUrl(), {
204
+ method: 'POST',
205
+ credentials: 'include',
206
+ headers: {
207
+ 'Content-Type': 'application/json'
208
+ },
209
+ body: jss.stringify(payload)
210
+ })
211
+
212
+ if (!response.ok) {
213
+ const error = await response.json().catch(() => ({ error: 'Unknown error' }))
214
+ throw new Error(error.error || `Request failed: ${response.status}`)
215
+ }
216
+
217
+ const result = jss.parse(await response.text())
218
+ return result.data
219
+ }
220
+
221
+ /**
222
+ * Close the streaming connection
223
+ */
224
+ function close() {
225
+ isActive = false
226
+
227
+ if (reconnectTimer) {
228
+ clearTimeout(reconnectTimer)
229
+ reconnectTimer = null
230
+ }
231
+
232
+ if (abortController) {
233
+ abortController.abort()
234
+ abortController = null
235
+ }
236
+
237
+ streamBuffer = ''
238
+ onClose()
239
+ }
240
+
241
+ return {
242
+ connect,
243
+ send,
244
+ close,
245
+ isConnected: () => isActive,
246
+ set onMessage(fn) { onMessage = fn },
247
+ set onOpen(fn) { onOpen = fn },
248
+ set onClose(fn) { onClose = fn },
249
+ set onError(fn) { onError = fn }
250
+ }
251
+ }
252
+
253
+ export { createStreamingTransport, configure, getPollUrl }