api-ape 2.2.2 → 2.3.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 +88 -17
- package/client/browser.js +7 -7
- package/client/connectSocket.js +257 -22
- package/client/index.js +3 -3
- package/dist/ape.js +1 -1
- package/dist/ape.js.map +3 -3
- package/dist/api-ape.min.js +1 -1
- package/dist/api-ape.min.js.map +3 -3
- package/index.d.ts +183 -19
- package/package.json +2 -2
- package/server/README.md +311 -5
- package/server/adapters/README.md +275 -0
- package/server/adapters/firebase.js +172 -0
- package/server/adapters/index.js +144 -0
- package/server/adapters/mongo.js +161 -0
- package/server/adapters/postgres.js +177 -0
- package/server/adapters/redis.js +154 -0
- package/server/adapters/supabase.js +199 -0
- package/server/index.js +3 -3
- package/server/lib/broadcast.js +115 -49
- package/server/lib/bun.js +4 -4
- package/server/lib/fileTransfer.js +129 -0
- package/server/lib/longPolling.js +22 -13
- package/server/lib/main.js +40 -8
- package/server/lib/wiring.js +23 -19
- package/server/socket/receive.js +46 -0
- package/server/socket/send.js +7 -0
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
# APE Cluster Adapters
|
|
2
|
+
|
|
3
|
+
Connect multiple api-ape server instances via a shared database for horizontal scaling.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```js
|
|
8
|
+
import ape from 'api-ape/server';
|
|
9
|
+
import { createClient } from 'redis';
|
|
10
|
+
|
|
11
|
+
// Connect to your database
|
|
12
|
+
const redis = createClient();
|
|
13
|
+
await redis.connect();
|
|
14
|
+
|
|
15
|
+
// Join the cluster — APE creates its own namespace
|
|
16
|
+
ape.joinVia(redis);
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
That's it. APE will:
|
|
20
|
+
- Detect the database type (Redis, MongoDB, PostgreSQL)
|
|
21
|
+
- Create namespaced keys/tables (`ape:*` or `ape_*`)
|
|
22
|
+
- Route messages between servers automatically
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## How It Works
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
┌─────────────┐ ┌─────────────┐
|
|
30
|
+
│ Server A │ │ Server B │
|
|
31
|
+
│ client-1 │ │ client-2 │
|
|
32
|
+
└──────┬──────┘ └──────▲──────┘
|
|
33
|
+
│ │
|
|
34
|
+
│ 1. sendTo("client-2") │
|
|
35
|
+
│ → lookup.read("client-2") │
|
|
36
|
+
│ → returns "srv-B" │
|
|
37
|
+
│ │
|
|
38
|
+
│ 2. channels.push("srv-B", msg) │
|
|
39
|
+
└──────────┬───────────────────────┘
|
|
40
|
+
│
|
|
41
|
+
┌──────▼──────┐
|
|
42
|
+
│ Database │
|
|
43
|
+
│ (message │
|
|
44
|
+
│ bus) │
|
|
45
|
+
└─────────────┘
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Messages are routed **directly** to the server hosting the client. No broadcast spam.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Adapter Interface
|
|
53
|
+
|
|
54
|
+
All adapters implement this interface:
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
interface AdapterInstance {
|
|
58
|
+
// Lifecycle
|
|
59
|
+
join(serverId: string): Promise<void>;
|
|
60
|
+
leave(): Promise<void>;
|
|
61
|
+
|
|
62
|
+
// Client → Server mapping
|
|
63
|
+
lookup: {
|
|
64
|
+
add(clientId: string): Promise<void>;
|
|
65
|
+
read(clientId: string): Promise<string | null>;
|
|
66
|
+
remove(clientId: string): Promise<void>;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// Inter-server messaging
|
|
70
|
+
channels: {
|
|
71
|
+
push(serverId: string, message: object): Promise<void>;
|
|
72
|
+
pull(serverId: string, handler: (msg, senderServerId) => void): Promise<() => void>;
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Supported Databases
|
|
80
|
+
|
|
81
|
+
### Redis (Recommended)
|
|
82
|
+
|
|
83
|
+
Best performance. Uses PUB/SUB for real-time messaging.
|
|
84
|
+
|
|
85
|
+
```js
|
|
86
|
+
import { createClient } from 'redis';
|
|
87
|
+
|
|
88
|
+
const redis = createClient({ url: 'redis://localhost:6379' });
|
|
89
|
+
await redis.connect();
|
|
90
|
+
|
|
91
|
+
ape.joinVia(redis);
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
**Keys created:**
|
|
95
|
+
- `ape:client:{clientId}` — client→server mapping
|
|
96
|
+
- `ape:channel:{serverId}` — PUB/SUB channel
|
|
97
|
+
- `ape:channel:ALL` — broadcast channel
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
### MongoDB
|
|
102
|
+
|
|
103
|
+
Uses Change Streams for real-time push (requires replica set).
|
|
104
|
+
|
|
105
|
+
```js
|
|
106
|
+
import { MongoClient } from 'mongodb';
|
|
107
|
+
|
|
108
|
+
const mongo = new MongoClient('mongodb://localhost:27017');
|
|
109
|
+
await mongo.connect();
|
|
110
|
+
|
|
111
|
+
ape.joinVia(mongo);
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
**Database/Collections created:**
|
|
115
|
+
- Database: `ape_cluster`
|
|
116
|
+
- Collection: `clients` — client→server mapping
|
|
117
|
+
- Collection: `events` — message bus (change streams)
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
### PostgreSQL
|
|
122
|
+
|
|
123
|
+
Uses LISTEN/NOTIFY for real-time messaging.
|
|
124
|
+
|
|
125
|
+
```js
|
|
126
|
+
import pg from 'pg';
|
|
127
|
+
|
|
128
|
+
const pool = new pg.Pool({ connectionString: 'postgres://localhost/mydb' });
|
|
129
|
+
|
|
130
|
+
ape.joinVia(pool);
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**Database/Tables created:**
|
|
134
|
+
- Database: `ape_cluster` (or uses existing)
|
|
135
|
+
- Table: `clients` — client→server mapping
|
|
136
|
+
- Channel: `ape_events` — LISTEN/NOTIFY channel
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
### Supabase
|
|
141
|
+
|
|
142
|
+
Uses Supabase Realtime for push messaging. Simple setup if you're already using Supabase.
|
|
143
|
+
|
|
144
|
+
```js
|
|
145
|
+
import { createClient } from '@supabase/supabase-js';
|
|
146
|
+
|
|
147
|
+
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);
|
|
148
|
+
|
|
149
|
+
ape.joinVia(supabase);
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
**Requirements:**
|
|
153
|
+
- Create table: `ape_clients (client_id TEXT PRIMARY KEY, server_id TEXT, updated_at TIMESTAMP)`
|
|
154
|
+
- Enable Realtime on your project
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
### Firebase Realtime Database
|
|
159
|
+
|
|
160
|
+
Native real-time push. Perfect for serverless and edge deployments.
|
|
161
|
+
|
|
162
|
+
```js
|
|
163
|
+
import { initializeApp } from 'firebase-admin/app';
|
|
164
|
+
import { getDatabase } from 'firebase-admin/database';
|
|
165
|
+
|
|
166
|
+
const app = initializeApp();
|
|
167
|
+
const database = getDatabase(app);
|
|
168
|
+
|
|
169
|
+
ape.joinVia(database);
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
**Paths created:**
|
|
173
|
+
- `/ape/clients/{clientId}` — client→server mapping
|
|
174
|
+
- `/ape/channels/{serverId}` — message channels
|
|
175
|
+
- `/ape/channels/ALL` — broadcast channel
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## Custom Adapters
|
|
180
|
+
|
|
181
|
+
For other databases or testing, pass your own adapter:
|
|
182
|
+
|
|
183
|
+
```js
|
|
184
|
+
ape.joinVia({
|
|
185
|
+
async join(serverId) {
|
|
186
|
+
// Subscribe to channels, register server
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
async leave() {
|
|
190
|
+
// Cleanup subscriptions, remove client mappings
|
|
191
|
+
},
|
|
192
|
+
|
|
193
|
+
lookup: {
|
|
194
|
+
async add(clientId) {
|
|
195
|
+
// Map clientId → this serverId
|
|
196
|
+
},
|
|
197
|
+
async read(clientId) {
|
|
198
|
+
// Return serverId or null
|
|
199
|
+
},
|
|
200
|
+
async remove(clientId) {
|
|
201
|
+
// Delete mapping (only if we own it)
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
|
|
205
|
+
channels: {
|
|
206
|
+
async push(serverId, message) {
|
|
207
|
+
// Send to serverId's channel ("" = broadcast)
|
|
208
|
+
},
|
|
209
|
+
async pull(serverId, handler) {
|
|
210
|
+
// Subscribe to serverId's channel
|
|
211
|
+
// handler(message, senderServerId)
|
|
212
|
+
return async () => { /* unsubscribe */ };
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## Options
|
|
221
|
+
|
|
222
|
+
```js
|
|
223
|
+
ape.joinVia(redis, {
|
|
224
|
+
namespace: 'myapp', // Default: 'ape'
|
|
225
|
+
serverId: 'srv-custom' // Default: auto-generated UUID
|
|
226
|
+
});
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## Lifecycle
|
|
232
|
+
|
|
233
|
+
| Event | Adapter Action |
|
|
234
|
+
|-------|---------------|
|
|
235
|
+
| Server starts | `join(serverId)` — subscribe to channels |
|
|
236
|
+
| Client connects | `lookup.add(clientId)` — register mapping |
|
|
237
|
+
| Client disconnects | `lookup.remove(clientId)` — delete mapping |
|
|
238
|
+
| Server shutdown | `leave()` — cleanup all owned mappings |
|
|
239
|
+
|
|
240
|
+
### Graceful Shutdown
|
|
241
|
+
|
|
242
|
+
```js
|
|
243
|
+
process.on('SIGINT', async () => {
|
|
244
|
+
await ape.leaveCluster();
|
|
245
|
+
process.exit(0);
|
|
246
|
+
});
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## Message Format
|
|
252
|
+
|
|
253
|
+
Messages sent via `channels.push`:
|
|
254
|
+
|
|
255
|
+
```js
|
|
256
|
+
// Direct message
|
|
257
|
+
{
|
|
258
|
+
destClientId: 'user-123',
|
|
259
|
+
type: 'chat',
|
|
260
|
+
data: { text: 'Hello!' }
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Broadcast
|
|
264
|
+
{
|
|
265
|
+
type: 'system',
|
|
266
|
+
data: { notice: 'Maintenance in 5 min' }
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Broadcast excluding sender
|
|
270
|
+
{
|
|
271
|
+
type: 'chat',
|
|
272
|
+
data: { text: 'Hello everyone!' },
|
|
273
|
+
excludeClientId: 'user-456'
|
|
274
|
+
}
|
|
275
|
+
```
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Firebase Realtime Database Adapter for APE Cluster
|
|
3
|
+
*
|
|
4
|
+
* Uses Firebase RTDB for real-time inter-server messaging.
|
|
5
|
+
* Perfect for serverless and edge deployments.
|
|
6
|
+
*
|
|
7
|
+
* Firebase provides native real-time push via onValue/onChildAdded listeners.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Create Firebase RTDB adapter
|
|
12
|
+
* @param {Database} database - Firebase Realtime Database instance from firebase-admin or firebase
|
|
13
|
+
* @param {object} opts
|
|
14
|
+
* @param {string} opts.serverId - This server's unique ID
|
|
15
|
+
* @param {string} [opts.namespace='ape'] - Path prefix
|
|
16
|
+
* @returns {Promise<AdapterInstance>}
|
|
17
|
+
*/
|
|
18
|
+
async function createFirebaseAdapter(database, { serverId, namespace = 'ape' }) {
|
|
19
|
+
if (!serverId) throw new Error('serverId required');
|
|
20
|
+
|
|
21
|
+
// State machine: INIT -> JOINED -> LEFT
|
|
22
|
+
let state = 'INIT';
|
|
23
|
+
const ownedClients = new Set();
|
|
24
|
+
const handlers = new Map();
|
|
25
|
+
const unsubscribers = [];
|
|
26
|
+
|
|
27
|
+
// Firebase path helpers
|
|
28
|
+
const paths = {
|
|
29
|
+
clients: () => `${namespace}/clients`,
|
|
30
|
+
client: (id) => `${namespace}/clients/${id}`,
|
|
31
|
+
channel: (sid) => `${namespace}/channels/${sid || 'ALL'}`,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// Get ref helper (works with both firebase-admin and firebase client SDK)
|
|
35
|
+
const ref = (path) => {
|
|
36
|
+
// firebase-admin style
|
|
37
|
+
if (typeof database.ref === 'function') {
|
|
38
|
+
return database.ref(path);
|
|
39
|
+
}
|
|
40
|
+
// firebase client SDK style (modular)
|
|
41
|
+
if (typeof database === 'object' && database._checkNotDeleted) {
|
|
42
|
+
const { ref: getRef } = require('firebase/database');
|
|
43
|
+
return getRef(database, path);
|
|
44
|
+
}
|
|
45
|
+
throw new Error('Unsupported Firebase Database instance');
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const adapter = {
|
|
49
|
+
get serverId() { return serverId; },
|
|
50
|
+
|
|
51
|
+
async join(id) {
|
|
52
|
+
const sid = id || serverId;
|
|
53
|
+
if (!sid?.trim()) throw new Error('serverId required');
|
|
54
|
+
if (state === 'JOINED') throw new Error('already joined');
|
|
55
|
+
if (state === 'LEFT') throw new Error('cannot rejoin after leave');
|
|
56
|
+
|
|
57
|
+
// Listen to this server's channel
|
|
58
|
+
const serverChannelRef = ref(paths.channel(sid));
|
|
59
|
+
const serverListener = serverChannelRef.on('child_added', (snapshot) => {
|
|
60
|
+
const data = snapshot.val();
|
|
61
|
+
if (data && data.senderServerId !== sid) {
|
|
62
|
+
const handler = handlers.get(sid) || handlers.get('');
|
|
63
|
+
if (handler) {
|
|
64
|
+
handler(data.message, data.senderServerId);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// Clean up processed message
|
|
68
|
+
snapshot.ref.remove();
|
|
69
|
+
});
|
|
70
|
+
unsubscribers.push(() => serverChannelRef.off('child_added', serverListener));
|
|
71
|
+
|
|
72
|
+
// Listen to broadcast channel
|
|
73
|
+
const broadcastRef = ref(paths.channel(''));
|
|
74
|
+
const broadcastListener = broadcastRef.on('child_added', (snapshot) => {
|
|
75
|
+
const data = snapshot.val();
|
|
76
|
+
if (data && data.senderServerId !== sid) {
|
|
77
|
+
const handler = handlers.get('');
|
|
78
|
+
if (handler) {
|
|
79
|
+
handler(data.message, data.senderServerId);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// Clean up processed message after short delay (let other servers read it)
|
|
83
|
+
setTimeout(() => snapshot.ref.remove(), 5000);
|
|
84
|
+
});
|
|
85
|
+
unsubscribers.push(() => broadcastRef.off('child_added', broadcastListener));
|
|
86
|
+
|
|
87
|
+
state = 'JOINED';
|
|
88
|
+
console.log(`✅ Firebase adapter: joined as ${sid}`);
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
async leave() {
|
|
92
|
+
if (state !== 'JOINED') return;
|
|
93
|
+
state = 'LEFT';
|
|
94
|
+
|
|
95
|
+
console.log(`🔴 Firebase adapter: leaving, cleaning up ${ownedClients.size} clients`);
|
|
96
|
+
|
|
97
|
+
// Unsubscribe all listeners
|
|
98
|
+
for (const unsub of unsubscribers) {
|
|
99
|
+
unsub();
|
|
100
|
+
}
|
|
101
|
+
unsubscribers.length = 0;
|
|
102
|
+
|
|
103
|
+
// Remove all owned client mappings
|
|
104
|
+
for (const clientId of ownedClients) {
|
|
105
|
+
try {
|
|
106
|
+
await ref(paths.client(clientId)).remove();
|
|
107
|
+
} catch (e) {
|
|
108
|
+
console.error(`Firebase: failed to remove client ${clientId}`, e.message);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
ownedClients.clear();
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
lookup: {
|
|
115
|
+
async add(clientId) {
|
|
116
|
+
await ref(paths.client(clientId)).set({
|
|
117
|
+
serverId,
|
|
118
|
+
updatedAt: Date.now()
|
|
119
|
+
});
|
|
120
|
+
ownedClients.add(clientId);
|
|
121
|
+
console.log(`📍 Firebase adapter: registered client ${clientId} -> ${serverId}`);
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
async read(clientId) {
|
|
125
|
+
const snapshot = await ref(paths.client(clientId)).once('value');
|
|
126
|
+
const data = snapshot.val();
|
|
127
|
+
return data?.serverId || null;
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
async remove(clientId) {
|
|
131
|
+
if (!ownedClients.has(clientId)) {
|
|
132
|
+
throw new Error(`not owner: cannot remove client ${clientId}`);
|
|
133
|
+
}
|
|
134
|
+
await ref(paths.client(clientId)).remove();
|
|
135
|
+
ownedClients.delete(clientId);
|
|
136
|
+
console.log(`🗑️ Firebase adapter: removed client ${clientId}`);
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
channels: {
|
|
141
|
+
async push(targetServerId, message) {
|
|
142
|
+
const channelRef = ref(paths.channel(targetServerId));
|
|
143
|
+
|
|
144
|
+
await channelRef.push({
|
|
145
|
+
targetServerId: targetServerId || '',
|
|
146
|
+
senderServerId: serverId,
|
|
147
|
+
message,
|
|
148
|
+
timestamp: Date.now()
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
if (targetServerId) {
|
|
152
|
+
console.log(`📤 Firebase adapter: pushed to server ${targetServerId}`);
|
|
153
|
+
} else {
|
|
154
|
+
console.log(`📢 Firebase adapter: broadcast to all servers`);
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
async pull(targetServerId, handler) {
|
|
159
|
+
handlers.set(targetServerId || '', handler);
|
|
160
|
+
|
|
161
|
+
// Return unsubscribe function
|
|
162
|
+
return async () => {
|
|
163
|
+
handlers.delete(targetServerId || '');
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
return adapter;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
module.exports = { createFirebaseAdapter };
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* APE Cluster Adapters
|
|
3
|
+
*
|
|
4
|
+
* Detect database type and create appropriate adapter for multi-server coordination.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* const adapter = await createAdapter(redisClient);
|
|
8
|
+
* const adapter = await createAdapter(mongoClient);
|
|
9
|
+
* const adapter = await createAdapter(pgPool);
|
|
10
|
+
* const adapter = await createAdapter(supabaseClient);
|
|
11
|
+
* const adapter = await createAdapter(firebaseDatabase);
|
|
12
|
+
* const adapter = await createAdapter(customAdapter);
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const { randomBytes } = require('crypto');
|
|
16
|
+
|
|
17
|
+
// Generate short unique server ID
|
|
18
|
+
const B = b => [...b].map(v => '0123456789ABCDEFGHJKMNPQRSTVWXYZ'[v & 31]).join('');
|
|
19
|
+
const uuid = () => B(randomBytes(8));
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Detect database type from client object
|
|
23
|
+
* @param {object} client - Database client
|
|
24
|
+
* @returns {'redis'|'mongo'|'postgres'|'supabase'|'firebase'|'custom'|null}
|
|
25
|
+
*/
|
|
26
|
+
function detectClientType(client) {
|
|
27
|
+
if (!client) return null;
|
|
28
|
+
|
|
29
|
+
// Custom adapter - has our interface methods
|
|
30
|
+
if (typeof client.join === 'function' &&
|
|
31
|
+
typeof client.leave === 'function' &&
|
|
32
|
+
client.lookup && client.channels) {
|
|
33
|
+
return 'custom';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Redis (node-redis or ioredis)
|
|
37
|
+
if (typeof client.duplicate === 'function' &&
|
|
38
|
+
(typeof client.publish === 'function' || typeof client.PUBLISH === 'function')) {
|
|
39
|
+
return 'redis';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// MongoDB
|
|
43
|
+
if (typeof client.db === 'function' && client.constructor?.name === 'MongoClient') {
|
|
44
|
+
return 'mongo';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// PostgreSQL (pg.Pool)
|
|
48
|
+
if (typeof client.query === 'function' && typeof client.connect === 'function') {
|
|
49
|
+
return 'postgres';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Supabase (has .from() for tables and .channel() for realtime)
|
|
53
|
+
if (typeof client.from === 'function' && typeof client.channel === 'function') {
|
|
54
|
+
return 'supabase';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Firebase Realtime Database (has .ref() method)
|
|
58
|
+
if (typeof client.ref === 'function' &&
|
|
59
|
+
(typeof client.goOnline === 'function' || client.app)) {
|
|
60
|
+
return 'firebase';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Create adapter from database client
|
|
68
|
+
* @param {object} client - Database client or custom adapter
|
|
69
|
+
* @param {object} opts - Options
|
|
70
|
+
* @param {string} [opts.namespace='ape'] - Key/table prefix
|
|
71
|
+
* @param {string} [opts.serverId] - Server ID (auto-generated if not provided)
|
|
72
|
+
* @returns {Promise<AdapterInstance>}
|
|
73
|
+
*/
|
|
74
|
+
async function createAdapter(client, opts = {}) {
|
|
75
|
+
const type = detectClientType(client);
|
|
76
|
+
const serverId = opts.serverId || uuid();
|
|
77
|
+
const namespace = opts.namespace || 'ape';
|
|
78
|
+
|
|
79
|
+
if (!type) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
'Unable to detect database type. Supported: Redis, MongoDB, PostgreSQL, Supabase, Firebase, or custom adapter.'
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
console.log(`🔌 APE: Detected ${type} adapter (serverId: ${serverId})`);
|
|
86
|
+
|
|
87
|
+
switch (type) {
|
|
88
|
+
case 'custom':
|
|
89
|
+
return wrapCustomAdapter(client, serverId);
|
|
90
|
+
|
|
91
|
+
case 'redis':
|
|
92
|
+
const { createRedisAdapter } = require('./redis');
|
|
93
|
+
return createRedisAdapter(client, { serverId, namespace });
|
|
94
|
+
|
|
95
|
+
case 'mongo':
|
|
96
|
+
const { createMongoAdapter } = require('./mongo');
|
|
97
|
+
return createMongoAdapter(client, { serverId, namespace });
|
|
98
|
+
|
|
99
|
+
case 'postgres':
|
|
100
|
+
const { createPostgresAdapter } = require('./postgres');
|
|
101
|
+
return createPostgresAdapter(client, { serverId, namespace });
|
|
102
|
+
|
|
103
|
+
case 'supabase':
|
|
104
|
+
const { createSupabaseAdapter } = require('./supabase');
|
|
105
|
+
return createSupabaseAdapter(client, { serverId, namespace });
|
|
106
|
+
|
|
107
|
+
case 'firebase':
|
|
108
|
+
const { createFirebaseAdapter } = require('./firebase');
|
|
109
|
+
return createFirebaseAdapter(client, { serverId, namespace });
|
|
110
|
+
|
|
111
|
+
default:
|
|
112
|
+
throw new Error(`Unknown adapter type: ${type}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Attach serverId to custom adapter
|
|
118
|
+
* @param {object} adapter - Custom adapter object
|
|
119
|
+
* @param {string} serverId - Server ID
|
|
120
|
+
* @returns {AdapterInstance}
|
|
121
|
+
*/
|
|
122
|
+
function wrapCustomAdapter(adapter, serverId) {
|
|
123
|
+
// Wrap to ensure consistent interface and default serverId
|
|
124
|
+
return {
|
|
125
|
+
get serverId() { return serverId; },
|
|
126
|
+
join: (id) => adapter.join(id || serverId),
|
|
127
|
+
leave: () => adapter.leave(),
|
|
128
|
+
lookup: {
|
|
129
|
+
add: (clientId) => adapter.lookup.add(clientId),
|
|
130
|
+
read: (clientId) => adapter.lookup.read(clientId),
|
|
131
|
+
remove: (clientId) => adapter.lookup.remove(clientId)
|
|
132
|
+
},
|
|
133
|
+
channels: {
|
|
134
|
+
push: (targetServerId, message) => adapter.channels.push(targetServerId, message),
|
|
135
|
+
pull: (targetServerId, handler) => adapter.channels.pull(targetServerId, handler)
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
module.exports = {
|
|
141
|
+
createAdapter,
|
|
142
|
+
detectClientType,
|
|
143
|
+
uuid
|
|
144
|
+
};
|