api-ape 3.0.2 → 4.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 +59 -572
- package/client/README.md +73 -14
- package/client/auth/crypto/aead.js +214 -0
- package/client/auth/crypto/constants.js +32 -0
- package/client/auth/crypto/encoding.js +104 -0
- package/client/auth/crypto/files.md +27 -0
- package/client/auth/crypto/kdf.js +217 -0
- package/client/auth/crypto-utils.js +118 -0
- package/client/auth/files.md +52 -0
- package/client/auth/key-recovery.js +288 -0
- package/client/auth/recovery/constants.js +37 -0
- package/client/auth/recovery/files.md +23 -0
- package/client/auth/recovery/key-derivation.js +61 -0
- package/client/auth/recovery/sss-browser.js +189 -0
- package/client/auth/share-storage.js +205 -0
- package/client/auth/storage/constants.js +18 -0
- package/client/auth/storage/db.js +132 -0
- package/client/auth/storage/files.md +27 -0
- package/client/auth/storage/keys.js +173 -0
- package/client/auth/storage/shares.js +200 -0
- package/client/browser.js +190 -23
- package/client/connectSocket.js +418 -988
- package/client/connection/README.md +23 -0
- package/client/connection/fileDownload.js +256 -0
- package/client/connection/fileHandling.js +450 -0
- package/client/connection/fileUtils.js +346 -0
- package/client/connection/files.md +71 -0
- package/client/connection/messageHandler.js +105 -0
- package/client/connection/network.js +350 -0
- package/client/connection/proxy.js +233 -0
- package/client/connection/sender.js +333 -0
- package/client/connection/state.js +321 -0
- package/client/connection/subscriptions.js +151 -0
- package/client/files.md +53 -0
- package/client/index.js +298 -142
- package/client/transports/README.md +50 -0
- package/client/transports/files.md +41 -0
- package/client/transports/streamParser.js +195 -0
- package/client/transports/streaming.js +555 -203
- package/dist/ape.js +6 -1
- package/dist/ape.js.map +4 -4
- package/index.d.ts +38 -16
- package/package.json +31 -6
- package/server/README.md +272 -67
- package/server/adapters/README.md +23 -14
- package/server/adapters/files.md +68 -0
- package/server/adapters/firebase.js +543 -160
- package/server/adapters/index.js +362 -112
- package/server/adapters/mongo.js +530 -140
- package/server/adapters/postgres.js +534 -155
- package/server/adapters/redis.js +508 -143
- package/server/adapters/supabase.js +555 -186
- package/server/client/README.md +43 -0
- package/server/client/connection.js +586 -0
- package/server/client/files.md +40 -0
- package/server/client/index.js +342 -0
- package/server/files.md +54 -0
- package/server/index.js +322 -71
- package/server/lib/README.md +26 -0
- package/server/lib/broadcast/clients.js +219 -0
- package/server/lib/broadcast/files.md +58 -0
- package/server/lib/broadcast/index.js +57 -0
- package/server/lib/broadcast/publishProxy.js +110 -0
- package/server/lib/broadcast/pubsub.js +137 -0
- package/server/lib/broadcast/sendProxy.js +103 -0
- package/server/lib/bun.js +315 -99
- package/server/lib/fileTransfer/README.md +63 -0
- package/server/lib/fileTransfer/files.md +30 -0
- package/server/lib/fileTransfer/streaming.js +435 -0
- package/server/lib/fileTransfer.js +710 -326
- package/server/lib/files.md +111 -0
- package/server/lib/httpUtils.js +283 -0
- package/server/lib/loader.js +208 -7
- package/server/lib/longPolling/README.md +63 -0
- package/server/lib/longPolling/files.md +44 -0
- package/server/lib/longPolling/getHandler.js +365 -0
- package/server/lib/longPolling/postHandler.js +327 -0
- package/server/lib/longPolling.js +174 -219
- package/server/lib/main.js +369 -532
- package/server/lib/runtimes/README.md +42 -0
- package/server/lib/runtimes/bun.js +586 -0
- package/server/lib/runtimes/files.md +56 -0
- package/server/lib/runtimes/node.js +511 -0
- package/server/lib/wiring.js +539 -98
- package/server/lib/ws/README.md +35 -0
- package/server/lib/ws/adapters/README.md +54 -0
- package/server/lib/ws/adapters/bun.js +538 -170
- package/server/lib/ws/adapters/deno.js +623 -149
- package/server/lib/ws/adapters/files.md +42 -0
- package/server/lib/ws/files.md +74 -0
- package/server/lib/ws/frames.js +532 -154
- package/server/lib/ws/index.js +207 -10
- package/server/lib/ws/server.js +385 -92
- package/server/lib/ws/socket.js +549 -181
- package/server/lib/wsProvider.js +363 -89
- package/server/plugins/binary.js +282 -0
- package/server/security/README.md +92 -0
- package/server/security/auth/README.md +319 -0
- package/server/security/auth/adapters/files.md +95 -0
- package/server/security/auth/adapters/ldap/constants.js +37 -0
- package/server/security/auth/adapters/ldap/files.md +19 -0
- package/server/security/auth/adapters/ldap/helpers.js +111 -0
- package/server/security/auth/adapters/ldap.js +353 -0
- package/server/security/auth/adapters/oauth2/constants.js +41 -0
- package/server/security/auth/adapters/oauth2/files.md +19 -0
- package/server/security/auth/adapters/oauth2/helpers.js +123 -0
- package/server/security/auth/adapters/oauth2.js +273 -0
- package/server/security/auth/adapters/opaque-handlers.js +314 -0
- package/server/security/auth/adapters/opaque.js +205 -0
- package/server/security/auth/adapters/saml/constants.js +52 -0
- package/server/security/auth/adapters/saml/files.md +19 -0
- package/server/security/auth/adapters/saml/helpers.js +74 -0
- package/server/security/auth/adapters/saml.js +173 -0
- package/server/security/auth/adapters/totp.js +703 -0
- package/server/security/auth/adapters/webauthn.js +625 -0
- package/server/security/auth/files.md +61 -0
- package/server/security/auth/framework/constants.js +27 -0
- package/server/security/auth/framework/files.md +23 -0
- package/server/security/auth/framework/handlers.js +272 -0
- package/server/security/auth/framework/socket-auth.js +177 -0
- package/server/security/auth/handlers/auth-messages.js +143 -0
- package/server/security/auth/handlers/files.md +28 -0
- package/server/security/auth/index.js +290 -0
- package/server/security/auth/mfa/crypto/aead.js +148 -0
- package/server/security/auth/mfa/crypto/constants.js +35 -0
- package/server/security/auth/mfa/crypto/files.md +27 -0
- package/server/security/auth/mfa/crypto/kdf.js +120 -0
- package/server/security/auth/mfa/crypto/utils.js +68 -0
- package/server/security/auth/mfa/crypto-utils.js +80 -0
- package/server/security/auth/mfa/files.md +77 -0
- package/server/security/auth/mfa/ledger/constants.js +75 -0
- package/server/security/auth/mfa/ledger/errors.js +73 -0
- package/server/security/auth/mfa/ledger/files.md +23 -0
- package/server/security/auth/mfa/ledger/share-record.js +32 -0
- package/server/security/auth/mfa/ledger.js +255 -0
- package/server/security/auth/mfa/recovery/constants.js +67 -0
- package/server/security/auth/mfa/recovery/files.md +19 -0
- package/server/security/auth/mfa/recovery/handlers.js +216 -0
- package/server/security/auth/mfa/recovery.js +191 -0
- package/server/security/auth/mfa/sss/constants.js +21 -0
- package/server/security/auth/mfa/sss/files.md +23 -0
- package/server/security/auth/mfa/sss/gf256.js +103 -0
- package/server/security/auth/mfa/sss/serialization.js +82 -0
- package/server/security/auth/mfa/sss.js +161 -0
- package/server/security/auth/mfa/two-of-three/constants.js +58 -0
- package/server/security/auth/mfa/two-of-three/files.md +23 -0
- package/server/security/auth/mfa/two-of-three/handlers.js +241 -0
- package/server/security/auth/mfa/two-of-three/helpers.js +71 -0
- package/server/security/auth/mfa/two-of-three.js +136 -0
- package/server/security/auth/nonce-manager.js +89 -0
- package/server/security/auth/state-machine-mfa.js +269 -0
- package/server/security/auth/state-machine.js +257 -0
- package/server/security/extractRootDomain.js +144 -16
- package/server/security/files.md +51 -0
- package/server/security/origin.js +197 -15
- package/server/security/reply.js +274 -16
- package/server/socket/README.md +119 -0
- package/server/socket/authMiddleware.js +299 -0
- package/server/socket/files.md +86 -0
- package/server/socket/open.js +154 -8
- package/server/socket/pluginHooks.js +334 -0
- package/server/socket/receive.js +184 -224
- package/server/socket/receiveContext.js +117 -0
- package/server/socket/send.js +416 -78
- package/server/socket/tagUtils.js +402 -0
- package/server/utils/README.md +19 -0
- package/server/utils/deepRequire.js +255 -30
- package/server/utils/files.md +57 -0
- package/server/utils/genId.js +182 -20
- package/server/utils/parseUserAgent.js +313 -251
- package/server/utils/userAgent/README.md +65 -0
- package/server/utils/userAgent/files.md +46 -0
- package/server/utils/userAgent/patterns.js +545 -0
- package/utils/README.md +21 -0
- package/utils/files.md +66 -0
- package/utils/jss/README.md +21 -0
- package/utils/jss/decode.js +471 -0
- package/utils/jss/encode.js +312 -0
- package/utils/jss/files.md +68 -0
- package/utils/jss/plugins.js +210 -0
- package/utils/jss.js +219 -273
- package/utils/messageHash.js +238 -35
- package/dist/api-ape.min.js +0 -2
- package/dist/api-ape.min.js.map +0 -7
- package/server/client.js +0 -311
- package/server/lib/broadcast.js +0 -146
|
@@ -1,172 +1,555 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Firebase Realtime Database Adapter for APE Cluster
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
2
|
+
* @fileoverview Firebase Realtime Database Adapter for APE Cluster
|
|
3
|
+
*
|
|
4
|
+
* This adapter enables multi-server api-ape deployments using Firebase Realtime Database
|
|
5
|
+
* as the coordination backend. Firebase RTDB provides native real-time push capabilities
|
|
6
|
+
* via `onValue` and `onChildAdded` listeners, making it ideal for serverless and edge
|
|
7
|
+
* deployments where traditional databases may not be available.
|
|
8
|
+
*
|
|
9
|
+
* Features:
|
|
10
|
+
* - **Real-time messaging**: Uses Firebase's native push notifications for instant delivery
|
|
11
|
+
* - **Serverless-friendly**: Works well with Cloud Functions, Vercel Edge, Cloudflare Workers
|
|
12
|
+
* - **Auto-cleanup**: Messages are automatically cleaned up after processing
|
|
13
|
+
* - **State machine**: Prevents invalid state transitions (INIT → JOINED → LEFT)
|
|
14
|
+
*
|
|
15
|
+
* Data Structure in Firebase:
|
|
16
|
+
* ```
|
|
17
|
+
* {namespace}/
|
|
18
|
+
* clients/
|
|
19
|
+
* {clientId}/
|
|
20
|
+
* serverId: "server-xyz"
|
|
21
|
+
* updatedAt: 1234567890
|
|
22
|
+
* channels/
|
|
23
|
+
* {serverId}/
|
|
24
|
+
* {pushId}/
|
|
25
|
+
* targetServerId: "server-xyz"
|
|
26
|
+
* senderServerId: "server-abc"
|
|
27
|
+
* message: {...}
|
|
28
|
+
* timestamp: 1234567890
|
|
29
|
+
* ALL/
|
|
30
|
+
* {pushId}/
|
|
31
|
+
* ...broadcast messages...
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
34
|
+
* @module server/adapters/firebase
|
|
35
|
+
* @see {@link module:server/adapters} - Main adapter factory
|
|
36
|
+
* @see {@link https://firebase.google.com/docs/database} - Firebase RTDB documentation
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* // Using with firebase-admin (Node.js server)
|
|
40
|
+
* const admin = require('firebase-admin')
|
|
41
|
+
* admin.initializeApp({ credential: admin.credential.cert(serviceAccount) })
|
|
42
|
+
* const database = admin.database()
|
|
43
|
+
*
|
|
44
|
+
* const { createFirebaseAdapter } = require('api-ape/server/adapters/firebase')
|
|
45
|
+
* const adapter = await createFirebaseAdapter(database, { serverId: 'server-1' })
|
|
46
|
+
* await adapter.join()
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* // Using with firebase client SDK (client-side or hybrid)
|
|
50
|
+
* import { getDatabase } from 'firebase/database'
|
|
51
|
+
* const database = getDatabase(app)
|
|
52
|
+
*
|
|
53
|
+
* const adapter = await createFirebaseAdapter(database, {
|
|
54
|
+
* serverId: 'edge-server-1',
|
|
55
|
+
* namespace: 'production'
|
|
56
|
+
* })
|
|
8
57
|
*/
|
|
9
58
|
|
|
10
59
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* @
|
|
60
|
+
* @typedef {Object} FirebaseDatabase
|
|
61
|
+
* Firebase Realtime Database instance. Can be either:
|
|
62
|
+
* - firebase-admin: `admin.database()` - Server-side SDK
|
|
63
|
+
* - firebase client: `getDatabase(app)` - Client/modular SDK
|
|
64
|
+
*
|
|
65
|
+
* @property {function(string): DatabaseReference} ref - Get a reference to a path
|
|
66
|
+
* @property {function=} goOnline - Reconnect to database (admin SDK)
|
|
67
|
+
* @property {Object=} app - Firebase app instance (client SDK)
|
|
17
68
|
*/
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* @typedef {Object} DatabaseReference
|
|
72
|
+
* Firebase database reference for a specific path.
|
|
73
|
+
*
|
|
74
|
+
* @property {function(string, function): function} on - Subscribe to events
|
|
75
|
+
* @property {function(string, function): void} off - Unsubscribe from events
|
|
76
|
+
* @property {function(string): Promise<DataSnapshot>} once - Read data once
|
|
77
|
+
* @property {function(Object): Promise<void>} set - Write data
|
|
78
|
+
* @property {function(Object): DatabaseReference} push - Push new child with auto-ID
|
|
79
|
+
* @property {function(): Promise<void>} remove - Delete this reference
|
|
80
|
+
*/
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* @typedef {Object} DataSnapshot
|
|
84
|
+
* Firebase data snapshot from a read operation.
|
|
85
|
+
*
|
|
86
|
+
* @property {function(): any} val - Get the data value
|
|
87
|
+
* @property {DatabaseReference} ref - Reference to this snapshot's location
|
|
88
|
+
*/
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* @typedef {Object} FirebaseAdapterOptions
|
|
92
|
+
* Configuration options for the Firebase adapter.
|
|
93
|
+
*
|
|
94
|
+
* @property {string} serverId - This server's unique identifier (required)
|
|
95
|
+
* @property {string} [namespace='ape'] - Path prefix in Firebase for all data.
|
|
96
|
+
* Use different namespaces to run multiple api-ape clusters on the same database.
|
|
97
|
+
*/
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* @typedef {Object} FirebaseAdapterInstance
|
|
101
|
+
* A configured Firebase adapter instance for cluster coordination.
|
|
102
|
+
*
|
|
103
|
+
* @property {string} serverId - This server's unique identifier (read-only getter)
|
|
104
|
+
* @property {function(string=): Promise<void>} join - Join the cluster and start listening
|
|
105
|
+
* @property {function(): Promise<void>} leave - Leave the cluster and clean up
|
|
106
|
+
* @property {Object} lookup - Client-to-server mapping operations
|
|
107
|
+
* @property {function(string): Promise<void>} lookup.add - Register a client
|
|
108
|
+
* @property {function(string): Promise<string|null>} lookup.read - Find client's server
|
|
109
|
+
* @property {function(string): Promise<void>} lookup.remove - Remove a client mapping
|
|
110
|
+
* @property {Object} channels - Inter-server messaging
|
|
111
|
+
* @property {function(string, Object): Promise<void>} channels.push - Send message
|
|
112
|
+
* @property {function(string, function): Promise<function>} channels.pull - Subscribe to messages
|
|
113
|
+
*/
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* @typedef {'INIT'|'JOINED'|'LEFT'} AdapterState
|
|
117
|
+
* State machine states for the adapter lifecycle:
|
|
118
|
+
* - INIT: Initial state, not yet joined
|
|
119
|
+
* - JOINED: Successfully joined the cluster
|
|
120
|
+
* - LEFT: Left the cluster, cannot rejoin (create new adapter instead)
|
|
121
|
+
*/
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Creates a Firebase Realtime Database adapter for APE cluster coordination.
|
|
125
|
+
*
|
|
126
|
+
* This function sets up Firebase listeners and provides a unified interface for:
|
|
127
|
+
* - Tracking which clients are connected to which servers
|
|
128
|
+
* - Sending messages between servers in the cluster
|
|
129
|
+
* - Broadcasting messages to all servers
|
|
130
|
+
*
|
|
131
|
+
* The adapter uses a state machine to ensure proper lifecycle management:
|
|
132
|
+
* 1. INIT → JOINED: Call `join()` to start listening for messages
|
|
133
|
+
* 2. JOINED → LEFT: Call `leave()` to clean up and disconnect
|
|
134
|
+
* 3. Cannot transition from LEFT back to JOINED (create new adapter)
|
|
135
|
+
*
|
|
136
|
+
* @async
|
|
137
|
+
* @function createFirebaseAdapter
|
|
138
|
+
* @param {FirebaseDatabase} database - Firebase Realtime Database instance
|
|
139
|
+
* @param {FirebaseAdapterOptions} options - Configuration options
|
|
140
|
+
* @param {string} options.serverId - This server's unique identifier
|
|
141
|
+
* @param {string} [options.namespace='ape'] - Path prefix for Firebase data
|
|
142
|
+
* @returns {Promise<FirebaseAdapterInstance>} Configured adapter instance
|
|
143
|
+
* @throws {Error} If serverId is not provided
|
|
144
|
+
*
|
|
145
|
+
* @example
|
|
146
|
+
* // Basic setup with firebase-admin
|
|
147
|
+
* const admin = require('firebase-admin')
|
|
148
|
+
* admin.initializeApp()
|
|
149
|
+
*
|
|
150
|
+
* const { createFirebaseAdapter } = require('api-ape/server/adapters/firebase')
|
|
151
|
+
* const adapter = await createFirebaseAdapter(admin.database(), {
|
|
152
|
+
* serverId: 'api-server-1'
|
|
153
|
+
* })
|
|
154
|
+
*
|
|
155
|
+
* // Join the cluster
|
|
156
|
+
* await adapter.join()
|
|
157
|
+
*
|
|
158
|
+
* // Register a connected client
|
|
159
|
+
* await adapter.lookup.add('client-abc-123')
|
|
160
|
+
*
|
|
161
|
+
* // Send a message to another server
|
|
162
|
+
* await adapter.channels.push('api-server-2', {
|
|
163
|
+
* type: 'forward',
|
|
164
|
+
* clientId: 'client-xyz',
|
|
165
|
+
* data: { message: 'Hello!' }
|
|
166
|
+
* })
|
|
167
|
+
*
|
|
168
|
+
* // Broadcast to all servers
|
|
169
|
+
* await adapter.channels.push('', { type: 'sync', data: {...} })
|
|
170
|
+
*
|
|
171
|
+
* // Subscribe to messages
|
|
172
|
+
* const unsubscribe = await adapter.channels.pull('', (msg, senderId) => {
|
|
173
|
+
* console.log(`Received from ${senderId}:`, msg)
|
|
174
|
+
* })
|
|
175
|
+
*
|
|
176
|
+
* // Clean up on shutdown
|
|
177
|
+
* process.on('SIGTERM', async () => {
|
|
178
|
+
* await adapter.leave()
|
|
179
|
+
* })
|
|
180
|
+
*/
|
|
181
|
+
async function createFirebaseAdapter(
|
|
182
|
+
database,
|
|
183
|
+
{ serverId, namespace = "ape" },
|
|
184
|
+
) {
|
|
185
|
+
if (!serverId) throw new Error("serverId required");
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Current adapter state (INIT → JOINED → LEFT)
|
|
189
|
+
* @type {AdapterState}
|
|
190
|
+
* @private
|
|
191
|
+
*/
|
|
192
|
+
let state = "INIT";
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Set of client IDs owned by this server.
|
|
196
|
+
* Used during cleanup to remove only our own client mappings.
|
|
197
|
+
* @type {Set<string>}
|
|
198
|
+
* @private
|
|
199
|
+
*/
|
|
200
|
+
const ownedClients = new Set();
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Map of channel handlers keyed by target server ID.
|
|
204
|
+
* Empty string key represents the broadcast channel.
|
|
205
|
+
* @type {Map<string, function(Object, string): void>}
|
|
206
|
+
* @private
|
|
207
|
+
*/
|
|
208
|
+
const handlers = new Map();
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Array of unsubscribe functions for Firebase listeners.
|
|
212
|
+
* Called during `leave()` to clean up all subscriptions.
|
|
213
|
+
* @type {Array<function(): void>}
|
|
214
|
+
* @private
|
|
215
|
+
*/
|
|
216
|
+
const unsubscribers = [];
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Firebase path helpers.
|
|
220
|
+
* Generates consistent paths for clients and channels.
|
|
221
|
+
* @private
|
|
222
|
+
*/
|
|
223
|
+
const paths = {
|
|
224
|
+
/**
|
|
225
|
+
* Get the path to the clients collection.
|
|
226
|
+
* @returns {string} Firebase path
|
|
227
|
+
*/
|
|
228
|
+
clients: () => `${namespace}/clients`,
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Get the path to a specific client.
|
|
232
|
+
* @param {string} id - Client ID
|
|
233
|
+
* @returns {string} Firebase path
|
|
234
|
+
*/
|
|
235
|
+
client: (id) => `${namespace}/clients/${id}`,
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Get the path to a server's message channel.
|
|
239
|
+
* Empty or null ID returns the broadcast channel "ALL".
|
|
240
|
+
* @param {string|null} sid - Server ID or empty for broadcast
|
|
241
|
+
* @returns {string} Firebase path
|
|
242
|
+
*/
|
|
243
|
+
channel: (sid) => `${namespace}/channels/${sid || "ALL"}`,
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Get a database reference helper.
|
|
248
|
+
* Supports both firebase-admin and firebase client SDK styles.
|
|
249
|
+
*
|
|
250
|
+
* @private
|
|
251
|
+
* @function ref
|
|
252
|
+
* @param {string} path - Firebase path
|
|
253
|
+
* @returns {DatabaseReference} Database reference
|
|
254
|
+
* @throws {Error} If database instance is unsupported
|
|
255
|
+
*/
|
|
256
|
+
const ref = (path) => {
|
|
257
|
+
// firebase-admin style (has .ref() method directly)
|
|
258
|
+
if (typeof database.ref === "function") {
|
|
259
|
+
return database.ref(path);
|
|
260
|
+
}
|
|
261
|
+
// firebase client SDK style (modular API)
|
|
262
|
+
if (typeof database === "object" && database._checkNotDeleted) {
|
|
263
|
+
const { ref: getRef } = require("firebase/database");
|
|
264
|
+
return getRef(database, path);
|
|
265
|
+
}
|
|
266
|
+
throw new Error("Unsupported Firebase Database instance");
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* The adapter instance with all public methods.
|
|
271
|
+
* @type {FirebaseAdapterInstance}
|
|
272
|
+
*/
|
|
273
|
+
const adapter = {
|
|
274
|
+
/**
|
|
275
|
+
* Get this server's unique identifier.
|
|
276
|
+
* @type {string}
|
|
277
|
+
* @readonly
|
|
278
|
+
*/
|
|
279
|
+
get serverId() {
|
|
280
|
+
return serverId;
|
|
281
|
+
},
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Join the cluster and start listening for messages.
|
|
285
|
+
*
|
|
286
|
+
* Sets up Firebase listeners for:
|
|
287
|
+
* - This server's direct message channel
|
|
288
|
+
* - The broadcast channel (ALL)
|
|
289
|
+
*
|
|
290
|
+
* Messages are automatically cleaned up after processing:
|
|
291
|
+
* - Direct messages: Deleted immediately after handling
|
|
292
|
+
* - Broadcast messages: Deleted after 5 seconds (allows other servers to receive)
|
|
293
|
+
*
|
|
294
|
+
* @async
|
|
295
|
+
* @param {string} [id] - Optional server ID override (defaults to constructor serverId)
|
|
296
|
+
* @returns {Promise<void>}
|
|
297
|
+
* @throws {Error} If already joined or previously left
|
|
298
|
+
*
|
|
299
|
+
* @example
|
|
300
|
+
* await adapter.join()
|
|
301
|
+
* console.log('Joined cluster as:', adapter.serverId)
|
|
302
|
+
*/
|
|
303
|
+
async join(id) {
|
|
304
|
+
const sid = id || serverId;
|
|
305
|
+
if (!sid?.trim()) throw new Error("serverId required");
|
|
306
|
+
if (state === "JOINED") throw new Error("already joined");
|
|
307
|
+
if (state === "LEFT") throw new Error("cannot rejoin after leave");
|
|
308
|
+
|
|
309
|
+
// Listen to this server's direct message channel
|
|
310
|
+
const serverChannelRef = ref(paths.channel(sid));
|
|
311
|
+
const serverListener = serverChannelRef.on("child_added", (snapshot) => {
|
|
312
|
+
const data = snapshot.val();
|
|
313
|
+
if (data && data.senderServerId !== sid) {
|
|
314
|
+
const handler = handlers.get(sid) || handlers.get("");
|
|
315
|
+
if (handler) {
|
|
316
|
+
handler(data.message, data.senderServerId);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
// Clean up processed message immediately
|
|
320
|
+
snapshot.ref.remove();
|
|
321
|
+
});
|
|
322
|
+
unsubscribers.push(() =>
|
|
323
|
+
serverChannelRef.off("child_added", serverListener),
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
// Listen to broadcast channel (ALL)
|
|
327
|
+
const broadcastRef = ref(paths.channel(""));
|
|
328
|
+
const broadcastListener = broadcastRef.on("child_added", (snapshot) => {
|
|
329
|
+
const data = snapshot.val();
|
|
330
|
+
if (data && data.senderServerId !== sid) {
|
|
331
|
+
const handler = handlers.get("");
|
|
332
|
+
if (handler) {
|
|
333
|
+
handler(data.message, data.senderServerId);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
// Clean up broadcast message after delay (let other servers read it)
|
|
337
|
+
setTimeout(() => snapshot.ref.remove(), 5000);
|
|
338
|
+
});
|
|
339
|
+
unsubscribers.push(() =>
|
|
340
|
+
broadcastRef.off("child_added", broadcastListener),
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
state = "JOINED";
|
|
344
|
+
console.log(`✅ Firebase adapter: joined as ${sid}`);
|
|
345
|
+
},
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Leave the cluster and clean up all resources.
|
|
349
|
+
*
|
|
350
|
+
* This method:
|
|
351
|
+
* 1. Unsubscribes all Firebase listeners
|
|
352
|
+
* 2. Removes all client mappings owned by this server
|
|
353
|
+
* 3. Transitions to LEFT state (cannot rejoin)
|
|
354
|
+
*
|
|
355
|
+
* @async
|
|
356
|
+
* @returns {Promise<void>}
|
|
357
|
+
*
|
|
358
|
+
* @example
|
|
359
|
+
* // Clean shutdown
|
|
360
|
+
* process.on('SIGTERM', async () => {
|
|
361
|
+
* await adapter.leave()
|
|
362
|
+
* process.exit(0)
|
|
363
|
+
* })
|
|
364
|
+
*/
|
|
365
|
+
async leave() {
|
|
366
|
+
if (state !== "JOINED") return;
|
|
367
|
+
state = "LEFT";
|
|
368
|
+
|
|
369
|
+
console.log(
|
|
370
|
+
`🔴 Firebase adapter: leaving, cleaning up ${ownedClients.size} clients`,
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
// Unsubscribe all listeners
|
|
374
|
+
for (const unsub of unsubscribers) {
|
|
375
|
+
unsub();
|
|
376
|
+
}
|
|
377
|
+
unsubscribers.length = 0;
|
|
378
|
+
|
|
379
|
+
// Remove all owned client mappings
|
|
380
|
+
for (const clientId of ownedClients) {
|
|
381
|
+
try {
|
|
382
|
+
await ref(paths.client(clientId)).remove();
|
|
383
|
+
} catch (e) {
|
|
384
|
+
console.error(
|
|
385
|
+
`Firebase: failed to remove client ${clientId}`,
|
|
386
|
+
e.message,
|
|
387
|
+
);
|
|
39
388
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
389
|
+
}
|
|
390
|
+
ownedClients.clear();
|
|
391
|
+
},
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Client-to-server mapping operations.
|
|
395
|
+
* Used to track which clients are connected to which servers.
|
|
396
|
+
*/
|
|
397
|
+
lookup: {
|
|
398
|
+
/**
|
|
399
|
+
* Register a client as owned by this server.
|
|
400
|
+
* Creates or updates the client mapping in Firebase.
|
|
401
|
+
*
|
|
402
|
+
* @async
|
|
403
|
+
* @param {string} clientId - The client's unique identifier
|
|
404
|
+
* @returns {Promise<void>}
|
|
405
|
+
*
|
|
406
|
+
* @example
|
|
407
|
+
* // When a client connects
|
|
408
|
+
* ws.on('connection', async (socket) => {
|
|
409
|
+
* const clientId = generateClientId()
|
|
410
|
+
* await adapter.lookup.add(clientId)
|
|
411
|
+
* })
|
|
412
|
+
*/
|
|
413
|
+
async add(clientId) {
|
|
414
|
+
await ref(paths.client(clientId)).set({
|
|
415
|
+
serverId,
|
|
416
|
+
updatedAt: Date.now(),
|
|
417
|
+
});
|
|
418
|
+
ownedClients.add(clientId);
|
|
419
|
+
console.log(
|
|
420
|
+
`📍 Firebase adapter: registered client ${clientId} -> ${serverId}`,
|
|
421
|
+
);
|
|
422
|
+
},
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Look up which server owns a client.
|
|
426
|
+
*
|
|
427
|
+
* @async
|
|
428
|
+
* @param {string} clientId - The client's unique identifier
|
|
429
|
+
* @returns {Promise<string|null>} Server ID owning the client, or null if not found
|
|
430
|
+
*
|
|
431
|
+
* @example
|
|
432
|
+
* // Route message to correct server
|
|
433
|
+
* const targetServer = await adapter.lookup.read(targetClientId)
|
|
434
|
+
* if (targetServer && targetServer !== adapter.serverId) {
|
|
435
|
+
* await adapter.channels.push(targetServer, message)
|
|
436
|
+
* }
|
|
437
|
+
*/
|
|
438
|
+
async read(clientId) {
|
|
439
|
+
const snapshot = await ref(paths.client(clientId)).once("value");
|
|
440
|
+
const data = snapshot.val();
|
|
441
|
+
return data?.serverId || null;
|
|
442
|
+
},
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Remove a client mapping.
|
|
446
|
+
* Can only remove clients owned by this server (security).
|
|
447
|
+
*
|
|
448
|
+
* @async
|
|
449
|
+
* @param {string} clientId - The client's unique identifier
|
|
450
|
+
* @returns {Promise<void>}
|
|
451
|
+
* @throws {Error} If this server doesn't own the client
|
|
452
|
+
*
|
|
453
|
+
* @example
|
|
454
|
+
* // When a client disconnects
|
|
455
|
+
* ws.on('close', async () => {
|
|
456
|
+
* await adapter.lookup.remove(clientId)
|
|
457
|
+
* })
|
|
458
|
+
*/
|
|
459
|
+
async remove(clientId) {
|
|
460
|
+
if (!ownedClients.has(clientId)) {
|
|
461
|
+
throw new Error(`not owner: cannot remove client ${clientId}`);
|
|
44
462
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
}
|
|
463
|
+
await ref(paths.client(clientId)).remove();
|
|
464
|
+
ownedClients.delete(clientId);
|
|
465
|
+
console.log(`🗑️ Firebase adapter: removed client ${clientId}`);
|
|
466
|
+
},
|
|
467
|
+
},
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Inter-server messaging operations.
|
|
471
|
+
* Used to send messages between servers in the cluster.
|
|
472
|
+
*/
|
|
473
|
+
channels: {
|
|
474
|
+
/**
|
|
475
|
+
* Send a message to a specific server or broadcast to all.
|
|
476
|
+
*
|
|
477
|
+
* @async
|
|
478
|
+
* @param {string} targetServerId - Target server ID, or empty string for broadcast
|
|
479
|
+
* @param {Object} message - Message payload (will be JSON-serialized)
|
|
480
|
+
* @returns {Promise<void>}
|
|
481
|
+
*
|
|
482
|
+
* @example
|
|
483
|
+
* // Send to specific server
|
|
484
|
+
* await adapter.channels.push('server-2', {
|
|
485
|
+
* type: 'forward',
|
|
486
|
+
* clientId: 'client-123',
|
|
487
|
+
* data: { text: 'Hello!' }
|
|
488
|
+
* })
|
|
489
|
+
*
|
|
490
|
+
* @example
|
|
491
|
+
* // Broadcast to all servers
|
|
492
|
+
* await adapter.channels.push('', {
|
|
493
|
+
* type: 'sync',
|
|
494
|
+
* data: { config: {...} }
|
|
495
|
+
* })
|
|
496
|
+
*/
|
|
497
|
+
async push(targetServerId, message) {
|
|
498
|
+
const channelRef = ref(paths.channel(targetServerId));
|
|
499
|
+
|
|
500
|
+
await channelRef.push({
|
|
501
|
+
targetServerId: targetServerId || "",
|
|
502
|
+
senderServerId: serverId,
|
|
503
|
+
message,
|
|
504
|
+
timestamp: Date.now(),
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
if (targetServerId) {
|
|
508
|
+
console.log(
|
|
509
|
+
`📤 Firebase adapter: pushed to server ${targetServerId}`,
|
|
510
|
+
);
|
|
511
|
+
} else {
|
|
512
|
+
console.log(`📢 Firebase adapter: broadcast to all servers`);
|
|
166
513
|
}
|
|
167
|
-
|
|
514
|
+
},
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Subscribe to messages for a specific channel.
|
|
518
|
+
*
|
|
519
|
+
* @async
|
|
520
|
+
* @param {string} targetServerId - Server ID to listen for, or empty for broadcast
|
|
521
|
+
* @param {function(Object, string): void} handler - Callback for received messages
|
|
522
|
+
* - First argument: The message payload
|
|
523
|
+
* - Second argument: The sender's server ID
|
|
524
|
+
* @returns {Promise<function(): Promise<void>>} Unsubscribe function
|
|
525
|
+
*
|
|
526
|
+
* @example
|
|
527
|
+
* // Listen for broadcast messages
|
|
528
|
+
* const unsubscribe = await adapter.channels.pull('', (message, senderId) => {
|
|
529
|
+
* console.log(`Broadcast from ${senderId}:`, message)
|
|
530
|
+
* })
|
|
531
|
+
*
|
|
532
|
+
* // Later, stop listening
|
|
533
|
+
* await unsubscribe()
|
|
534
|
+
*
|
|
535
|
+
* @example
|
|
536
|
+
* // Listen for direct messages
|
|
537
|
+
* await adapter.channels.pull(adapter.serverId, (message, senderId) => {
|
|
538
|
+
* console.log(`Direct message from ${senderId}:`, message)
|
|
539
|
+
* })
|
|
540
|
+
*/
|
|
541
|
+
async pull(targetServerId, handler) {
|
|
542
|
+
handlers.set(targetServerId || "", handler);
|
|
543
|
+
|
|
544
|
+
// Return unsubscribe function
|
|
545
|
+
return async () => {
|
|
546
|
+
handlers.delete(targetServerId || "");
|
|
547
|
+
};
|
|
548
|
+
},
|
|
549
|
+
},
|
|
550
|
+
};
|
|
168
551
|
|
|
169
|
-
|
|
552
|
+
return adapter;
|
|
170
553
|
}
|
|
171
554
|
|
|
172
555
|
module.exports = { createFirebaseAdapter };
|