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.
- package/README.md +114 -11
- package/client/browser.js +19 -1
- package/client/connectSocket.js +556 -368
- package/client/transports/streaming.js +253 -0
- package/dist/ape.js +651 -301
- package/example/Bun/README.md +74 -0
- package/example/Bun/api/message.ts +11 -0
- package/example/Bun/index.html +76 -0
- package/example/Bun/package.json +9 -0
- package/example/Bun/server.ts +59 -0
- package/example/Bun/styles.css +128 -0
- package/example/ExpressJs/README.md +5 -7
- package/example/ExpressJs/backend.js +23 -21
- package/example/NextJs/ape/client.js +3 -3
- package/example/NextJs/ape/onConnect.js +5 -5
- package/example/NextJs/package-lock.json +1353 -60
- package/example/NextJs/package.json +0 -1
- package/example/NextJs/pages/index.tsx +21 -10
- package/example/NextJs/server.js +7 -11
- package/example/README.md +51 -0
- package/example/Vite/README.md +68 -0
- package/example/Vite/ape/client.ts +66 -0
- package/example/Vite/ape/onConnect.ts +52 -0
- package/example/Vite/api/message.ts +57 -0
- package/example/Vite/index.html +16 -0
- package/example/Vite/package.json +19 -0
- package/example/Vite/server.ts +62 -0
- package/example/Vite/src/App.vue +170 -0
- package/example/Vite/src/components/Info.vue +352 -0
- package/example/Vite/src/main.ts +5 -0
- package/example/Vite/src/style.css +200 -0
- package/example/Vite/src/vite-env.d.ts +7 -0
- package/example/Vite/vite.config.ts +20 -0
- package/index.d.ts +40 -3
- package/package.json +26 -11
- package/server/README.md +54 -9
- package/server/index.js +10 -2
- package/server/lib/longPolling.js +221 -0
- package/server/lib/main.js +172 -60
- package/server/security/origin.js +16 -4
- 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 }
|