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.
Files changed (89) hide show
  1. package/README.md +203 -124
  2. package/client/README.md +37 -30
  3. package/client/browser.js +10 -8
  4. package/client/connectSocket.js +662 -381
  5. package/client/index.js +171 -0
  6. package/client/transports/streaming.js +240 -0
  7. package/dist/ape.js +2 -699
  8. package/dist/ape.js.map +7 -0
  9. package/dist/api-ape.min.js +2 -0
  10. package/dist/api-ape.min.js.map +7 -0
  11. package/index.d.ts +71 -18
  12. package/package.json +50 -15
  13. package/server/README.md +99 -13
  14. package/server/lib/broadcast.js +25 -8
  15. package/server/lib/bun.js +122 -0
  16. package/server/lib/longPolling.js +226 -0
  17. package/server/lib/main.js +381 -38
  18. package/server/lib/wiring.js +19 -12
  19. package/server/lib/ws/adapters/bun.js +225 -0
  20. package/server/lib/ws/adapters/deno.js +186 -0
  21. package/server/lib/ws/frames.js +217 -0
  22. package/server/lib/ws/index.js +15 -0
  23. package/server/lib/ws/server.js +109 -0
  24. package/server/lib/ws/socket.js +222 -0
  25. package/server/lib/wsProvider.js +135 -0
  26. package/server/security/origin.js +16 -4
  27. package/server/socket/receive.js +14 -1
  28. package/server/socket/send.js +6 -6
  29. package/server/utils/deepRequire.js +25 -10
  30. package/server/utils/parseUserAgent.js +286 -0
  31. package/example/Bun/README.md +0 -74
  32. package/example/Bun/api/message.ts +0 -11
  33. package/example/Bun/index.html +0 -76
  34. package/example/Bun/package.json +0 -9
  35. package/example/Bun/server.ts +0 -59
  36. package/example/Bun/styles.css +0 -128
  37. package/example/ExpressJs/README.md +0 -95
  38. package/example/ExpressJs/api/message.js +0 -11
  39. package/example/ExpressJs/backend.js +0 -39
  40. package/example/ExpressJs/index.html +0 -88
  41. package/example/ExpressJs/package-lock.json +0 -834
  42. package/example/ExpressJs/package.json +0 -10
  43. package/example/ExpressJs/styles.css +0 -128
  44. package/example/NextJs/.dockerignore +0 -29
  45. package/example/NextJs/Dockerfile +0 -52
  46. package/example/NextJs/Dockerfile.dev +0 -27
  47. package/example/NextJs/README.md +0 -113
  48. package/example/NextJs/ape/client.js +0 -66
  49. package/example/NextJs/ape/embed.js +0 -12
  50. package/example/NextJs/ape/index.js +0 -23
  51. package/example/NextJs/ape/logic/chat.js +0 -62
  52. package/example/NextJs/ape/onConnect.js +0 -69
  53. package/example/NextJs/ape/onDisconnect.js +0 -13
  54. package/example/NextJs/ape/onError.js +0 -9
  55. package/example/NextJs/ape/onReceive.js +0 -15
  56. package/example/NextJs/ape/onSend.js +0 -15
  57. package/example/NextJs/api/message.js +0 -44
  58. package/example/NextJs/docker-compose.yml +0 -22
  59. package/example/NextJs/next-env.d.ts +0 -5
  60. package/example/NextJs/next.config.js +0 -8
  61. package/example/NextJs/package-lock.json +0 -6400
  62. package/example/NextJs/package.json +0 -24
  63. package/example/NextJs/pages/Info.tsx +0 -153
  64. package/example/NextJs/pages/_app.tsx +0 -6
  65. package/example/NextJs/pages/index.tsx +0 -275
  66. package/example/NextJs/public/favicon.ico +0 -0
  67. package/example/NextJs/public/vercel.svg +0 -4
  68. package/example/NextJs/server.js +0 -36
  69. package/example/NextJs/styles/Chat.module.css +0 -448
  70. package/example/NextJs/styles/Home.module.css +0 -129
  71. package/example/NextJs/styles/globals.css +0 -26
  72. package/example/NextJs/tsconfig.json +0 -20
  73. package/example/README.md +0 -117
  74. package/example/Vite/README.md +0 -68
  75. package/example/Vite/ape/client.ts +0 -66
  76. package/example/Vite/ape/onConnect.ts +0 -52
  77. package/example/Vite/api/message.ts +0 -57
  78. package/example/Vite/index.html +0 -16
  79. package/example/Vite/package.json +0 -19
  80. package/example/Vite/server.ts +0 -62
  81. package/example/Vite/src/App.vue +0 -170
  82. package/example/Vite/src/components/Info.vue +0 -352
  83. package/example/Vite/src/main.ts +0 -5
  84. package/example/Vite/src/style.css +0 -200
  85. package/example/Vite/src/vite-env.d.ts +0 -7
  86. package/example/Vite/vite.config.ts +0 -20
  87. package/todo.md +0 -85
  88. package/utils/jss.test.js +0 -261
  89. package/utils/messageHash.test.js +0 -56
@@ -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 }