api-ape 2.0.0 → 2.2.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 +203 -124
- package/client/README.md +37 -30
- package/client/browser.js +10 -8
- package/client/connectSocket.js +662 -381
- package/client/index.js +171 -0
- package/client/transports/streaming.js +240 -0
- package/dist/ape.js +2 -699
- package/dist/ape.js.map +7 -0
- package/dist/api-ape.min.js +2 -0
- package/dist/api-ape.min.js.map +7 -0
- package/index.d.ts +71 -18
- package/package.json +50 -15
- package/server/README.md +99 -13
- package/server/lib/broadcast.js +25 -8
- package/server/lib/bun.js +122 -0
- package/server/lib/longPolling.js +226 -0
- package/server/lib/main.js +381 -38
- package/server/lib/wiring.js +19 -12
- package/server/lib/ws/adapters/bun.js +225 -0
- package/server/lib/ws/adapters/deno.js +186 -0
- package/server/lib/ws/frames.js +217 -0
- package/server/lib/ws/index.js +15 -0
- package/server/lib/ws/server.js +109 -0
- package/server/lib/ws/socket.js +222 -0
- package/server/lib/wsProvider.js +135 -0
- package/server/security/origin.js +16 -4
- package/server/socket/receive.js +14 -1
- package/server/socket/send.js +6 -6
- package/server/utils/deepRequire.js +25 -10
- package/server/utils/parseUserAgent.js +286 -0
- package/example/Bun/README.md +0 -74
- package/example/Bun/api/message.ts +0 -11
- package/example/Bun/index.html +0 -76
- package/example/Bun/package.json +0 -9
- package/example/Bun/server.ts +0 -59
- package/example/Bun/styles.css +0 -128
- package/example/ExpressJs/README.md +0 -95
- package/example/ExpressJs/api/message.js +0 -11
- package/example/ExpressJs/backend.js +0 -39
- package/example/ExpressJs/index.html +0 -88
- package/example/ExpressJs/package-lock.json +0 -834
- package/example/ExpressJs/package.json +0 -10
- package/example/ExpressJs/styles.css +0 -128
- package/example/NextJs/.dockerignore +0 -29
- package/example/NextJs/Dockerfile +0 -52
- package/example/NextJs/Dockerfile.dev +0 -27
- package/example/NextJs/README.md +0 -113
- package/example/NextJs/ape/client.js +0 -66
- package/example/NextJs/ape/embed.js +0 -12
- package/example/NextJs/ape/index.js +0 -23
- package/example/NextJs/ape/logic/chat.js +0 -62
- package/example/NextJs/ape/onConnect.js +0 -69
- package/example/NextJs/ape/onDisconnect.js +0 -13
- package/example/NextJs/ape/onError.js +0 -9
- package/example/NextJs/ape/onReceive.js +0 -15
- package/example/NextJs/ape/onSend.js +0 -15
- package/example/NextJs/api/message.js +0 -44
- package/example/NextJs/docker-compose.yml +0 -22
- package/example/NextJs/next-env.d.ts +0 -5
- package/example/NextJs/next.config.js +0 -8
- package/example/NextJs/package-lock.json +0 -6400
- package/example/NextJs/package.json +0 -24
- package/example/NextJs/pages/Info.tsx +0 -153
- package/example/NextJs/pages/_app.tsx +0 -6
- package/example/NextJs/pages/index.tsx +0 -275
- package/example/NextJs/public/favicon.ico +0 -0
- package/example/NextJs/public/vercel.svg +0 -4
- package/example/NextJs/server.js +0 -36
- package/example/NextJs/styles/Chat.module.css +0 -448
- package/example/NextJs/styles/Home.module.css +0 -129
- package/example/NextJs/styles/globals.css +0 -26
- package/example/NextJs/tsconfig.json +0 -20
- package/example/README.md +0 -117
- package/example/Vite/README.md +0 -68
- package/example/Vite/ape/client.ts +0 -66
- package/example/Vite/ape/onConnect.ts +0 -52
- package/example/Vite/api/message.ts +0 -57
- package/example/Vite/index.html +0 -16
- package/example/Vite/package.json +0 -19
- package/example/Vite/server.ts +0 -62
- package/example/Vite/src/App.vue +0 -170
- package/example/Vite/src/components/Info.vue +0 -352
- package/example/Vite/src/main.ts +0 -5
- package/example/Vite/src/style.css +0 -200
- package/example/Vite/src/vite-env.d.ts +0 -7
- package/example/Vite/vite.config.ts +0 -20
- package/todo.md +0 -85
- package/utils/jss.test.js +0 -261
- package/utils/messageHash.test.js +0 -56
package/client/index.js
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified api-ape export for browser
|
|
3
|
+
*
|
|
4
|
+
* Auto-detects browser environment, initializes client, and buffers
|
|
5
|
+
* calls until the connection is ready. No more getApeClient().then()!
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* import api from 'api-ape'
|
|
9
|
+
*
|
|
10
|
+
* // Properties are proxied - calls buffer until connected
|
|
11
|
+
* api.message({ user: 'Bob', text: 'Hello!' })
|
|
12
|
+
*
|
|
13
|
+
* // Subscribe to broadcasts
|
|
14
|
+
* api.on('message', (data) => console.log(data))
|
|
15
|
+
*
|
|
16
|
+
* // Check connection state
|
|
17
|
+
* api.onConnectionChange((state) => console.log(state))
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
// Only run this in browser environments
|
|
21
|
+
const isBrowser = typeof window !== 'undefined' && typeof document !== 'undefined'
|
|
22
|
+
|
|
23
|
+
let clientPromise = null
|
|
24
|
+
let resolvedClient = null
|
|
25
|
+
const bufferedCalls = []
|
|
26
|
+
const bufferedReceivers = []
|
|
27
|
+
const connectionChangeHandlers = []
|
|
28
|
+
let currentConnectionState = 'disconnected'
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Initialize the client (called once on first use)
|
|
32
|
+
*/
|
|
33
|
+
function getClient() {
|
|
34
|
+
if (clientPromise) return clientPromise
|
|
35
|
+
|
|
36
|
+
if (!isBrowser) {
|
|
37
|
+
// Return a dummy object for SSR
|
|
38
|
+
return Promise.resolve(null)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
clientPromise = (async () => {
|
|
42
|
+
const connectSocket = (await import('./connectSocket.js')).default
|
|
43
|
+
|
|
44
|
+
// Connect
|
|
45
|
+
const client = connectSocket()
|
|
46
|
+
connectSocket.autoReconnect()
|
|
47
|
+
|
|
48
|
+
// Track connection state
|
|
49
|
+
client.onConnectionChange((state) => {
|
|
50
|
+
currentConnectionState = state
|
|
51
|
+
connectionChangeHandlers.forEach(fn => fn(state))
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
resolvedClient = client
|
|
55
|
+
|
|
56
|
+
// Flush buffered receivers
|
|
57
|
+
bufferedReceivers.forEach(({ type, handler }) => {
|
|
58
|
+
client.setOnReciver(type, handler)
|
|
59
|
+
})
|
|
60
|
+
bufferedReceivers.length = 0
|
|
61
|
+
|
|
62
|
+
// Flush buffered calls
|
|
63
|
+
bufferedCalls.forEach(({ method, args, resolve, reject }) => {
|
|
64
|
+
try {
|
|
65
|
+
const result = client.sender[method](...args)
|
|
66
|
+
if (result && typeof result.then === 'function') {
|
|
67
|
+
result.then(resolve).catch(reject)
|
|
68
|
+
} else {
|
|
69
|
+
resolve(result)
|
|
70
|
+
}
|
|
71
|
+
} catch (err) {
|
|
72
|
+
reject(err)
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
bufferedCalls.length = 0
|
|
76
|
+
|
|
77
|
+
return client
|
|
78
|
+
})()
|
|
79
|
+
|
|
80
|
+
return clientPromise
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Create a sender proxy that buffers calls until client is ready
|
|
85
|
+
*/
|
|
86
|
+
const senderProxy = new Proxy({}, {
|
|
87
|
+
get(target, prop) {
|
|
88
|
+
// Reserved properties
|
|
89
|
+
if (prop === 'on') return on
|
|
90
|
+
if (prop === 'onConnectionChange') return onConnectionChange
|
|
91
|
+
if (prop === 'getTransport') return () => resolvedClient?.getTransport?.() || null
|
|
92
|
+
if (prop === 'then' || prop === 'catch') return undefined // Not a Promise
|
|
93
|
+
|
|
94
|
+
// Return a function that either calls directly or buffers
|
|
95
|
+
return (...args) => {
|
|
96
|
+
// If client is ready, call directly
|
|
97
|
+
if (resolvedClient) {
|
|
98
|
+
return resolvedClient.sender[prop](...args)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Buffer the call and return a Promise
|
|
102
|
+
return new Promise((resolve, reject) => {
|
|
103
|
+
bufferedCalls.push({ method: prop, args, resolve, reject })
|
|
104
|
+
// Ensure client is initializing
|
|
105
|
+
getClient()
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Subscribe to broadcasts from the server
|
|
113
|
+
* @param {string} type - Broadcast type to listen for
|
|
114
|
+
* @param {Function} handler - Handler function
|
|
115
|
+
*/
|
|
116
|
+
function on(type, handler) {
|
|
117
|
+
if (resolvedClient) {
|
|
118
|
+
resolvedClient.setOnReciver(type, handler)
|
|
119
|
+
} else {
|
|
120
|
+
bufferedReceivers.push({ type, handler })
|
|
121
|
+
getClient()
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Subscribe to connection state changes
|
|
127
|
+
* @param {Function} handler - Called with state: 'offline' | 'walled' | 'disconnected' | 'connecting' | 'connected'
|
|
128
|
+
* @returns {Function} Unsubscribe function
|
|
129
|
+
*/
|
|
130
|
+
function onConnectionChange(handler) {
|
|
131
|
+
connectionChangeHandlers.push(handler)
|
|
132
|
+
// Immediately call with current state
|
|
133
|
+
handler(currentConnectionState)
|
|
134
|
+
|
|
135
|
+
// If client exists, also register with it
|
|
136
|
+
if (resolvedClient) {
|
|
137
|
+
return resolvedClient.onConnectionChange(handler)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Ensure client is initializing
|
|
141
|
+
getClient()
|
|
142
|
+
|
|
143
|
+
// Return unsubscribe function
|
|
144
|
+
return () => {
|
|
145
|
+
const idx = connectionChangeHandlers.indexOf(handler)
|
|
146
|
+
if (idx > -1) connectionChangeHandlers.splice(idx, 1)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Define properties on the proxy to avoid Proxy interception issues
|
|
151
|
+
Object.defineProperty(senderProxy, 'on', {
|
|
152
|
+
value: on,
|
|
153
|
+
writable: false,
|
|
154
|
+
enumerable: false,
|
|
155
|
+
configurable: false
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
Object.defineProperty(senderProxy, 'onConnectionChange', {
|
|
159
|
+
value: onConnectionChange,
|
|
160
|
+
writable: false,
|
|
161
|
+
enumerable: false,
|
|
162
|
+
configurable: false
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
// Auto-initialize in browser
|
|
166
|
+
if (isBrowser) {
|
|
167
|
+
getClient()
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export default senderProxy
|
|
171
|
+
export { on, onConnectionChange, getClient }
|
|
@@ -0,0 +1,240 @@
|
|
|
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
|
+
/**
|
|
9
|
+
* Get base URL for polling endpoints
|
|
10
|
+
*/
|
|
11
|
+
function getPollUrl() {
|
|
12
|
+
const hostname = window.location.hostname
|
|
13
|
+
const localServers = ["localhost", "127.0.0.1", "[::1]"]
|
|
14
|
+
const isLocal = localServers.includes(hostname)
|
|
15
|
+
const isHttps = window.location.protocol === "https:"
|
|
16
|
+
|
|
17
|
+
const port = isLocal ? 9010 : (window.location.port || (isHttps ? 443 : 80))
|
|
18
|
+
|
|
19
|
+
const protocol = isHttps ? "https" : "http"
|
|
20
|
+
const portSuffix = (isLocal || (port !== 80 && port !== 443)) ? `:${port}` : ""
|
|
21
|
+
|
|
22
|
+
return `${protocol}://${hostname}${portSuffix}/api/ape/poll`
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Parse JSON objects from a streaming buffer by counting braces
|
|
27
|
+
* Handles strings containing braces correctly
|
|
28
|
+
*/
|
|
29
|
+
function parseStreamBuffer(buffer) {
|
|
30
|
+
const messages = []
|
|
31
|
+
let start = -1
|
|
32
|
+
let depth = 0
|
|
33
|
+
let inString = false
|
|
34
|
+
let escaped = false
|
|
35
|
+
|
|
36
|
+
for (let i = 0; i < buffer.length; i++) {
|
|
37
|
+
const char = buffer[i]
|
|
38
|
+
|
|
39
|
+
if (escaped) {
|
|
40
|
+
escaped = false
|
|
41
|
+
continue
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (char === '\\' && inString) {
|
|
45
|
+
escaped = true
|
|
46
|
+
continue
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (char === '"') {
|
|
50
|
+
inString = !inString
|
|
51
|
+
continue
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (inString) continue
|
|
55
|
+
|
|
56
|
+
if (char === '{') {
|
|
57
|
+
if (depth === 0) {
|
|
58
|
+
start = i
|
|
59
|
+
}
|
|
60
|
+
depth++
|
|
61
|
+
} else if (char === '}') {
|
|
62
|
+
depth--
|
|
63
|
+
if (depth === 0 && start !== -1) {
|
|
64
|
+
const jsonStr = buffer.slice(start, i + 1)
|
|
65
|
+
try {
|
|
66
|
+
messages.push(jss.parse(jsonStr))
|
|
67
|
+
} catch (e) {
|
|
68
|
+
console.error('🦍 Failed to parse stream message:', e)
|
|
69
|
+
}
|
|
70
|
+
start = -1
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Return remaining buffer (incomplete message)
|
|
76
|
+
const remaining = start !== -1 ? buffer.slice(start) : ''
|
|
77
|
+
return { messages, remaining }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Create streaming transport instance
|
|
82
|
+
*/
|
|
83
|
+
function createStreamingTransport() {
|
|
84
|
+
let isActive = false
|
|
85
|
+
let abortController = null
|
|
86
|
+
let streamBuffer = ''
|
|
87
|
+
let reconnectTimer = null
|
|
88
|
+
|
|
89
|
+
// Callbacks
|
|
90
|
+
let onMessage = () => { }
|
|
91
|
+
let onOpen = () => { }
|
|
92
|
+
let onClose = () => { }
|
|
93
|
+
let onError = () => { }
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Start the streaming connection
|
|
97
|
+
*/
|
|
98
|
+
async function connect() {
|
|
99
|
+
if (isActive) return
|
|
100
|
+
|
|
101
|
+
isActive = true
|
|
102
|
+
abortController = new AbortController()
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const response = await fetch(getPollUrl(), {
|
|
106
|
+
method: 'GET',
|
|
107
|
+
credentials: 'include',
|
|
108
|
+
signal: abortController.signal,
|
|
109
|
+
headers: {
|
|
110
|
+
'Accept': 'application/json'
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
if (!response.ok) {
|
|
115
|
+
throw new Error(`Stream connect failed: ${response.status}`)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
onOpen()
|
|
119
|
+
|
|
120
|
+
const reader = response.body.getReader()
|
|
121
|
+
const decoder = new TextDecoder()
|
|
122
|
+
|
|
123
|
+
async function read() {
|
|
124
|
+
while (isActive) {
|
|
125
|
+
try {
|
|
126
|
+
const { done, value } = await reader.read()
|
|
127
|
+
|
|
128
|
+
if (done) {
|
|
129
|
+
// Stream ended - reconnect
|
|
130
|
+
scheduleReconnect()
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
streamBuffer += decoder.decode(value, { stream: true })
|
|
135
|
+
const { messages, remaining } = parseStreamBuffer(streamBuffer)
|
|
136
|
+
streamBuffer = remaining
|
|
137
|
+
|
|
138
|
+
for (const msg of messages) {
|
|
139
|
+
// Skip heartbeat messages
|
|
140
|
+
if (msg.type === '__heartbeat__') continue
|
|
141
|
+
onMessage(msg)
|
|
142
|
+
}
|
|
143
|
+
} catch (readErr) {
|
|
144
|
+
if (readErr.name === 'AbortError') return
|
|
145
|
+
console.error('🦍 Stream read error:', readErr)
|
|
146
|
+
scheduleReconnect()
|
|
147
|
+
return
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
read()
|
|
153
|
+
|
|
154
|
+
} catch (err) {
|
|
155
|
+
if (err.name === 'AbortError') return
|
|
156
|
+
|
|
157
|
+
console.error('🦍 Stream connection error:', err)
|
|
158
|
+
onError(err)
|
|
159
|
+
scheduleReconnect()
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Schedule reconnection with small delay
|
|
165
|
+
*/
|
|
166
|
+
function scheduleReconnect() {
|
|
167
|
+
if (!isActive) return
|
|
168
|
+
|
|
169
|
+
if (reconnectTimer) {
|
|
170
|
+
clearTimeout(reconnectTimer)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
reconnectTimer = setTimeout(() => {
|
|
174
|
+
if (isActive) {
|
|
175
|
+
connect()
|
|
176
|
+
}
|
|
177
|
+
}, 500)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Send a message via POST
|
|
182
|
+
*/
|
|
183
|
+
async function send(type, data, createdAt) {
|
|
184
|
+
const payload = {
|
|
185
|
+
type,
|
|
186
|
+
data,
|
|
187
|
+
createdAt: new Date(createdAt)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const response = await fetch(getPollUrl(), {
|
|
191
|
+
method: 'POST',
|
|
192
|
+
credentials: 'include',
|
|
193
|
+
headers: {
|
|
194
|
+
'Content-Type': 'application/json'
|
|
195
|
+
},
|
|
196
|
+
body: jss.stringify(payload)
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
if (!response.ok) {
|
|
200
|
+
const error = await response.json().catch(() => ({ error: 'Unknown error' }))
|
|
201
|
+
throw new Error(error.error || `Request failed: ${response.status}`)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const result = jss.parse(await response.text())
|
|
205
|
+
return result.data
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Close the streaming connection
|
|
210
|
+
*/
|
|
211
|
+
function close() {
|
|
212
|
+
isActive = false
|
|
213
|
+
|
|
214
|
+
if (reconnectTimer) {
|
|
215
|
+
clearTimeout(reconnectTimer)
|
|
216
|
+
reconnectTimer = null
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (abortController) {
|
|
220
|
+
abortController.abort()
|
|
221
|
+
abortController = null
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
streamBuffer = ''
|
|
225
|
+
onClose()
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
connect,
|
|
230
|
+
send,
|
|
231
|
+
close,
|
|
232
|
+
isConnected: () => isActive,
|
|
233
|
+
set onMessage(fn) { onMessage = fn },
|
|
234
|
+
set onOpen(fn) { onOpen = fn },
|
|
235
|
+
set onClose(fn) { onClose = fn },
|
|
236
|
+
set onError(fn) { onError = fn }
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export { createStreamingTransport, getPollUrl }
|