@gokiteam/goki-dev 0.2.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.
Files changed (205) hide show
  1. package/README.md +478 -0
  2. package/bin/goki-dev.js +452 -0
  3. package/bin/mcp-server.js +16 -0
  4. package/bin/secrets-cli.js +302 -0
  5. package/cli/ComposeOverrideGenerator.js +226 -0
  6. package/cli/ComposeParser.js +73 -0
  7. package/cli/ConfigGenerator.js +304 -0
  8. package/cli/ConfigManager.js +46 -0
  9. package/cli/DatabaseManager.js +94 -0
  10. package/cli/DevToolsChecker.js +21 -0
  11. package/cli/DevToolsDir.js +66 -0
  12. package/cli/DevToolsManager.js +451 -0
  13. package/cli/DockerManager.js +138 -0
  14. package/cli/FunctionManager.js +95 -0
  15. package/cli/HttpProxyRewriter.js +91 -0
  16. package/cli/Logger.js +10 -0
  17. package/cli/McpConfigManager.js +123 -0
  18. package/cli/NgrokManager.js +431 -0
  19. package/cli/ProjectCLI.js +2322 -0
  20. package/cli/PubSubManager.js +129 -0
  21. package/cli/SnapshotManager.js +88 -0
  22. package/cli/UiFormatter.js +292 -0
  23. package/cli/WebhookUrlRewriter.js +32 -0
  24. package/cli/secrets/BiometricAuth.js +125 -0
  25. package/cli/secrets/SecretInjector.js +47 -0
  26. package/cli/secrets/SecretsConfig.js +141 -0
  27. package/cli/secrets/SecretsDoctor.js +384 -0
  28. package/cli/secrets/SecretsManager.js +255 -0
  29. package/client/dist/client.d.ts +332 -0
  30. package/client/dist/client.js +507 -0
  31. package/client/dist/helpers.d.ts +62 -0
  32. package/client/dist/helpers.js +122 -0
  33. package/client/dist/index.d.ts +59 -0
  34. package/client/dist/index.js +78 -0
  35. package/client/dist/package.json +1 -0
  36. package/client/dist/types.d.ts +280 -0
  37. package/client/dist/types.js +7 -0
  38. package/config.development +46 -0
  39. package/config.test +18 -0
  40. package/guidelines/CodingStyleGuideline.md +148 -0
  41. package/guidelines/CommentingGuideline.md +10 -0
  42. package/guidelines/HttpApiImplementationGuideline.md +137 -0
  43. package/guidelines/NamingGuideline.md +182 -0
  44. package/package.json +138 -0
  45. package/patterns/api/[collectionName]/Controllers.md +62 -0
  46. package/patterns/api/[collectionName]/Logic.md +154 -0
  47. package/patterns/api/[collectionName]/Permissions.md +81 -0
  48. package/patterns/api/[collectionName]/Router.md +83 -0
  49. package/patterns/api/[collectionName]/Schemas.md +197 -0
  50. package/patterns/configs/Patterns.md +7 -0
  51. package/patterns/enums/Patterns.md +24 -0
  52. package/patterns/errorHandling/Patterns.md +185 -0
  53. package/patterns/testing/Patterns.md +232 -0
  54. package/src/Server.js +238 -0
  55. package/src/api/dashboard/Controllers.js +9 -0
  56. package/src/api/dashboard/Logic.js +76 -0
  57. package/src/api/dashboard/Router.js +11 -0
  58. package/src/api/dashboard/Schemas.js +47 -0
  59. package/src/api/data/Controllers.js +26 -0
  60. package/src/api/data/Logic.js +188 -0
  61. package/src/api/data/Router.js +16 -0
  62. package/src/api/docker/Controllers.js +33 -0
  63. package/src/api/docker/Logic.js +268 -0
  64. package/src/api/docker/Router.js +15 -0
  65. package/src/api/docker/Schemas.js +80 -0
  66. package/src/api/docs/Controllers.js +15 -0
  67. package/src/api/docs/Logic.js +85 -0
  68. package/src/api/docs/Router.js +12 -0
  69. package/src/api/export/Controllers.js +30 -0
  70. package/src/api/export/Logic.js +143 -0
  71. package/src/api/export/Router.js +18 -0
  72. package/src/api/export/Schemas.js +104 -0
  73. package/src/api/firestore/Controllers.js +152 -0
  74. package/src/api/firestore/Logic.js +474 -0
  75. package/src/api/firestore/Router.js +23 -0
  76. package/src/api/functions/Controllers.js +261 -0
  77. package/src/api/functions/Logic.js +710 -0
  78. package/src/api/functions/Router.js +50 -0
  79. package/src/api/functions/Schemas.js +193 -0
  80. package/src/api/gateway/Controllers.js +72 -0
  81. package/src/api/gateway/Logic.js +74 -0
  82. package/src/api/gateway/Router.js +10 -0
  83. package/src/api/gateway/Schemas.js +19 -0
  84. package/src/api/health/Controllers.js +14 -0
  85. package/src/api/health/Logic.js +24 -0
  86. package/src/api/health/Router.js +12 -0
  87. package/src/api/httpTraffic/Controllers.js +29 -0
  88. package/src/api/httpTraffic/Logic.js +33 -0
  89. package/src/api/httpTraffic/Router.js +9 -0
  90. package/src/api/httpTraffic/Schemas.js +23 -0
  91. package/src/api/logging/Controllers.js +80 -0
  92. package/src/api/logging/Logic.js +461 -0
  93. package/src/api/logging/Router.js +24 -0
  94. package/src/api/logging/Schemas.js +43 -0
  95. package/src/api/mqtt/Controllers.js +17 -0
  96. package/src/api/mqtt/Logic.js +66 -0
  97. package/src/api/mqtt/Router.js +12 -0
  98. package/src/api/postgres/Controllers.js +97 -0
  99. package/src/api/postgres/Logic.js +221 -0
  100. package/src/api/postgres/Router.js +21 -0
  101. package/src/api/pubsub/Controllers.js +236 -0
  102. package/src/api/pubsub/Logic.js +732 -0
  103. package/src/api/pubsub/Router.js +41 -0
  104. package/src/api/pubsub/Schemas.js +355 -0
  105. package/src/api/redis/Controllers.js +63 -0
  106. package/src/api/redis/Logic.js +239 -0
  107. package/src/api/redis/Router.js +21 -0
  108. package/src/api/scheduler/Controllers.js +27 -0
  109. package/src/api/scheduler/Logic.js +49 -0
  110. package/src/api/scheduler/Router.js +16 -0
  111. package/src/api/services/Controllers.js +26 -0
  112. package/src/api/services/Logic.js +205 -0
  113. package/src/api/services/Router.js +14 -0
  114. package/src/api/services/Schemas.js +66 -0
  115. package/src/api/snapshots/Controllers.js +37 -0
  116. package/src/api/snapshots/Logic.js +797 -0
  117. package/src/api/snapshots/Router.js +15 -0
  118. package/src/api/snapshots/Schemas.js +23 -0
  119. package/src/api/webhooks/Controllers.js +49 -0
  120. package/src/api/webhooks/Logic.js +137 -0
  121. package/src/api/webhooks/Router.js +12 -0
  122. package/src/api/webhooks/Schemas.js +31 -0
  123. package/src/configs/Application.js +147 -0
  124. package/src/configs/Default.js +13 -0
  125. package/src/consumers/BlackboxLogsConsumer.js +235 -0
  126. package/src/consumers/DockerLogsConsumer.js +687 -0
  127. package/src/db/Tables.js +66 -0
  128. package/src/db/schemas/firestore.js +18 -0
  129. package/src/db/schemas/functions.js +65 -0
  130. package/src/db/schemas/httpTraffic.js +43 -0
  131. package/src/db/schemas/logging.js +74 -0
  132. package/src/db/schemas/migrations.js +64 -0
  133. package/src/db/schemas/mqtt.js +56 -0
  134. package/src/db/schemas/pubsub.js +90 -0
  135. package/src/db/schemas/pubsubRegistry.js +22 -0
  136. package/src/db/schemas/webhooks.js +28 -0
  137. package/src/emulation/awsiot/Controllers.js +91 -0
  138. package/src/emulation/awsiot/Logic.js +70 -0
  139. package/src/emulation/awsiot/Router.js +19 -0
  140. package/src/emulation/awsiot/Server.js +100 -0
  141. package/src/emulation/firestore/Server.js +136 -0
  142. package/src/emulation/logging/Controllers.js +212 -0
  143. package/src/emulation/logging/Logic.js +416 -0
  144. package/src/emulation/logging/Router.js +36 -0
  145. package/src/emulation/logging/Schemas.js +82 -0
  146. package/src/emulation/logging/Server.js +108 -0
  147. package/src/emulation/pubsub/Controllers.js +279 -0
  148. package/src/emulation/pubsub/DefaultTopics.js +162 -0
  149. package/src/emulation/pubsub/Logic.js +427 -0
  150. package/src/emulation/pubsub/README.md +309 -0
  151. package/src/emulation/pubsub/Router.js +33 -0
  152. package/src/emulation/pubsub/Server.js +104 -0
  153. package/src/emulation/pubsub/ShadowPoller.js +276 -0
  154. package/src/emulation/pubsub/ShadowSubscriptionManager.js +199 -0
  155. package/src/enums/ContainerNames.js +106 -0
  156. package/src/enums/ErrorReason.js +28 -0
  157. package/src/enums/FunctionStatuses.js +15 -0
  158. package/src/enums/FunctionTriggerTypes.js +15 -0
  159. package/src/enums/GatewayState.js +7 -0
  160. package/src/enums/ServiceNames.js +68 -0
  161. package/src/jobs/DatabaseMaintenance.js +184 -0
  162. package/src/jobs/MessageHistoryCleanup.js +152 -0
  163. package/src/mcp/ApiClient.js +25 -0
  164. package/src/mcp/Server.js +52 -0
  165. package/src/mcp/prompts/debugging.js +104 -0
  166. package/src/mcp/resources/platform.js +118 -0
  167. package/src/mcp/tools/data.js +84 -0
  168. package/src/mcp/tools/docker.js +166 -0
  169. package/src/mcp/tools/firestore.js +162 -0
  170. package/src/mcp/tools/functions.js +380 -0
  171. package/src/mcp/tools/httpTraffic.js +69 -0
  172. package/src/mcp/tools/logging.js +174 -0
  173. package/src/mcp/tools/mqtt.js +37 -0
  174. package/src/mcp/tools/postgres.js +130 -0
  175. package/src/mcp/tools/pubsub.js +316 -0
  176. package/src/mcp/tools/redis.js +146 -0
  177. package/src/mcp/tools/services.js +169 -0
  178. package/src/mcp/tools/snapshots.js +88 -0
  179. package/src/mcp/tools/webhooks.js +115 -0
  180. package/src/middleware/DevProxy.js +67 -0
  181. package/src/middleware/ErrorCatcher.js +35 -0
  182. package/src/middleware/HttpProxy.js +215 -0
  183. package/src/middleware/Reply.js +24 -0
  184. package/src/middleware/TraceId.js +9 -0
  185. package/src/middleware/WebhookProxy.js +234 -0
  186. package/src/protocols/mqtt/Broker.js +92 -0
  187. package/src/protocols/mqtt/Handlers.js +175 -0
  188. package/src/protocols/mqtt/PubSubBridge.js +162 -0
  189. package/src/protocols/mqtt/Server.js +116 -0
  190. package/src/runtime/FunctionRunner.js +179 -0
  191. package/src/services/AppGatewayService.js +582 -0
  192. package/src/singletons/FirestoreBroadcaster.js +367 -0
  193. package/src/singletons/FunctionTriggerDispatcher.js +456 -0
  194. package/src/singletons/FunctionsService.js +418 -0
  195. package/src/singletons/HttpProxy.js +224 -0
  196. package/src/singletons/LogBroadcaster.js +159 -0
  197. package/src/singletons/Logger.js +49 -0
  198. package/src/singletons/MemoryJsonStore.js +175 -0
  199. package/src/singletons/MessageBroadcaster.js +190 -0
  200. package/src/singletons/PostgresBroadcaster.js +367 -0
  201. package/src/singletons/PostgresClient.js +180 -0
  202. package/src/singletons/RedisClient.js +184 -0
  203. package/src/singletons/SqliteStore.js +480 -0
  204. package/src/singletons/TickService.js +151 -0
  205. package/src/singletons/WebhookProxy.js +223 -0
@@ -0,0 +1,367 @@
1
+ // FirestoreBroadcaster singleton for managing SSE connections to Firestore change streams
2
+ // Handles client registration, Firebase snapshot listeners with reference counting, and change broadcasting
3
+
4
+ import { Logger } from './Logger.js'
5
+ import { FunctionTriggerDispatcher } from './FunctionTriggerDispatcher.js'
6
+ import { initializeApp, getApps } from 'firebase-admin/app'
7
+ import { getFirestore } from 'firebase-admin/firestore'
8
+ import { Application } from '../configs/Application.js'
9
+
10
+ const DEFAULT_PROJECT_ID = Application.firestore.projectId
11
+
12
+ // Set emulator host environment variable for Firebase Admin SDK
13
+ process.env.FIRESTORE_EMULATOR_HOST = Application.firestore.emulatorHost
14
+
15
+ // Cache Firebase app instances by projectId
16
+ const appCache = new Map()
17
+
18
+ function getFirestoreDb (projectId) {
19
+ const pid = projectId || DEFAULT_PROJECT_ID
20
+ if (appCache.has(pid)) {
21
+ return appCache.get(pid)
22
+ }
23
+ const appName = `firestore-${pid}`
24
+ const existingApp = getApps().find(a => a.name === appName)
25
+ const app = existingApp || initializeApp({ projectId: pid }, appName)
26
+ const db = getFirestore(app)
27
+ appCache.set(pid, db)
28
+ return db
29
+ }
30
+
31
+ class FirestoreBroadcasterClass {
32
+ constructor () {
33
+ this.clients = new Map() // connectionId → { ctx, filters: { projectId, collectionPath } }
34
+ this.listeners = new Map() // listenerKey (projectId:collectionPath) → { unsubscribe, refCount }
35
+ this.heartbeatId = null
36
+ this.heartbeatIntervalMs = 30000
37
+ }
38
+
39
+ /**
40
+ * Register a new SSE client and start collection listener if needed
41
+ * @param {string} connectionId - Unique identifier for the connection (traceId)
42
+ * @param {Object} ctx - Koa context with res.write for SSE
43
+ * @param {Object} filters - { projectId, collectionPath }
44
+ */
45
+ addClient (connectionId, ctx, filters = {}) {
46
+ this.clients.set(connectionId, { ctx, filters })
47
+ this.startHeartbeat()
48
+
49
+ // Start listening to the collection if filters provided
50
+ if (filters.projectId && filters.collectionPath) {
51
+ this.startCollectionListener(filters.projectId, filters.collectionPath)
52
+ }
53
+
54
+ Logger.log({
55
+ level: 'info',
56
+ message: 'Firestore SSE client connected',
57
+ data: { connectionId, filters, totalClients: this.clients.size }
58
+ })
59
+ }
60
+
61
+ /**
62
+ * Unregister an SSE client and stop listener if no more clients watching
63
+ * @param {string} connectionId - Unique identifier for the connection
64
+ */
65
+ removeClient (connectionId) {
66
+ const client = this.clients.get(connectionId)
67
+ if (!client) return
68
+
69
+ const removed = this.clients.delete(connectionId)
70
+
71
+ if (removed) {
72
+ // Decrement listener ref count
73
+ if (client.filters.projectId && client.filters.collectionPath) {
74
+ this.decrementListenerRefCount(client.filters.projectId, client.filters.collectionPath)
75
+ }
76
+
77
+ Logger.log({
78
+ level: 'info',
79
+ message: 'Firestore SSE client disconnected',
80
+ data: { connectionId, totalClients: this.clients.size }
81
+ })
82
+
83
+ if (this.clients.size === 0) {
84
+ this.stopHeartbeat()
85
+ }
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Start a Firebase snapshot listener for a collection
91
+ * Uses reference counting to share one listener across multiple clients
92
+ * @param {string} projectId - Firestore project ID
93
+ * @param {string} collectionPath - Collection path
94
+ */
95
+ startCollectionListener (projectId, collectionPath) {
96
+ const listenerKey = `${projectId}:${collectionPath}`
97
+
98
+ // Increment ref count if listener already exists
99
+ if (this.listeners.has(listenerKey)) {
100
+ const listener = this.listeners.get(listenerKey)
101
+ listener.refCount++
102
+ Logger.log({
103
+ level: 'debug',
104
+ message: 'Firestore listener ref count incremented',
105
+ data: { projectId, collectionPath, refCount: listener.refCount }
106
+ })
107
+ return
108
+ }
109
+
110
+ // Create new snapshot listener
111
+ try {
112
+ const db = getFirestoreDb(projectId)
113
+ Logger.log({
114
+ level: 'info',
115
+ message: 'Creating Firestore snapshot listener',
116
+ data: { projectId, collectionPath, listenerKey }
117
+ })
118
+ const unsubscribe = db.collection(collectionPath).onSnapshot(
119
+ (snapshot) => {
120
+ const changeCount = snapshot.docChanges().length
121
+ Logger.log({
122
+ level: 'info',
123
+ message: 'Firestore snapshot received',
124
+ data: { projectId, collectionPath, changeCount }
125
+ })
126
+
127
+ snapshot.docChanges().forEach((change) => {
128
+ const changeEvent = {
129
+ eventId: `evt_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
130
+ timestamp: new Date().toISOString(),
131
+ projectId,
132
+ collectionPath,
133
+ documentId: change.doc.id,
134
+ changeType: change.type, // 'added', 'modified', 'removed'
135
+ document: change.type !== 'removed' ? {
136
+ name: change.doc.ref.path,
137
+ fields: change.doc.data(),
138
+ createTime: change.doc.createTime?.toDate().toISOString(),
139
+ updateTime: change.doc.updateTime?.toDate().toISOString()
140
+ } : null
141
+ }
142
+
143
+ Logger.log({
144
+ level: 'info',
145
+ message: 'Broadcasting Firestore change event',
146
+ data: { eventId: changeEvent.eventId, changeType: changeEvent.changeType, documentId: changeEvent.documentId, clientCount: this.clients.size }
147
+ })
148
+
149
+ this.broadcast(changeEvent)
150
+ })
151
+ },
152
+ (error) => {
153
+ Logger.log({
154
+ level: 'error',
155
+ message: 'Firestore snapshot listener error',
156
+ data: { projectId, collectionPath, error: error.message }
157
+ })
158
+ // Remove listener on error
159
+ this.stopCollectionListener(projectId, collectionPath)
160
+ }
161
+ )
162
+
163
+ this.listeners.set(listenerKey, { unsubscribe, refCount: 1 })
164
+
165
+ Logger.log({
166
+ level: 'info',
167
+ message: 'Firestore snapshot listener started',
168
+ data: { projectId, collectionPath }
169
+ })
170
+ } catch (error) {
171
+ Logger.log({
172
+ level: 'error',
173
+ message: 'Failed to start Firestore snapshot listener',
174
+ data: { projectId, collectionPath, error: error.message }
175
+ })
176
+ }
177
+ }
178
+
179
+ /**
180
+ * Decrement listener reference count and stop if reaches zero
181
+ * @param {string} projectId - Firestore project ID
182
+ * @param {string} collectionPath - Collection path
183
+ */
184
+ decrementListenerRefCount (projectId, collectionPath) {
185
+ const listenerKey = `${projectId}:${collectionPath}`
186
+ const listener = this.listeners.get(listenerKey)
187
+
188
+ if (!listener) return
189
+
190
+ listener.refCount--
191
+
192
+ Logger.log({
193
+ level: 'debug',
194
+ message: 'Firestore listener ref count decremented',
195
+ data: { projectId, collectionPath, refCount: listener.refCount }
196
+ })
197
+
198
+ if (listener.refCount <= 0) {
199
+ this.stopCollectionListener(projectId, collectionPath)
200
+ }
201
+ }
202
+
203
+ /**
204
+ * Stop a Firebase snapshot listener
205
+ * @param {string} projectId - Firestore project ID
206
+ * @param {string} collectionPath - Collection path
207
+ */
208
+ stopCollectionListener (projectId, collectionPath) {
209
+ const listenerKey = `${projectId}:${collectionPath}`
210
+ const listener = this.listeners.get(listenerKey)
211
+
212
+ if (listener && listener.unsubscribe) {
213
+ listener.unsubscribe()
214
+ this.listeners.delete(listenerKey)
215
+
216
+ Logger.log({
217
+ level: 'info',
218
+ message: 'Firestore snapshot listener stopped',
219
+ data: { projectId, collectionPath }
220
+ })
221
+ }
222
+ }
223
+
224
+ /**
225
+ * Broadcast a change event to all connected clients matching filters
226
+ * @param {Object} change - { eventId, timestamp, projectId, collectionPath, documentId, changeType, document }
227
+ */
228
+ broadcast (change) {
229
+ let sentCount = 0
230
+ for (const [connectionId, { ctx, filters }] of this.clients) {
231
+ if (this.matchesFilters(change, filters)) {
232
+ try {
233
+ this.sendSSE(ctx, 'change', change)
234
+ sentCount++
235
+ } catch (error) {
236
+ Logger.log({
237
+ level: 'error',
238
+ message: 'Failed to send SSE change event to client',
239
+ data: { connectionId, error: error.message }
240
+ })
241
+ this.removeClient(connectionId)
242
+ }
243
+ }
244
+ }
245
+ Logger.log({
246
+ level: 'debug',
247
+ message: 'Firestore change broadcasted to SSE clients',
248
+ data: {
249
+ eventId: change.eventId,
250
+ changeType: change.changeType,
251
+ documentId: change.documentId,
252
+ totalClients: this.clients.size,
253
+ sentToClients: sentCount
254
+ }
255
+ })
256
+ // Dispatch to Cloud Functions (regardless of SSE client count)
257
+ try {
258
+ if (FunctionTriggerDispatcher && typeof FunctionTriggerDispatcher.onFirestoreChange === 'function') {
259
+ FunctionTriggerDispatcher.onFirestoreChange(change).catch(err => {
260
+ Logger.log({ level: 'error', message: 'Cloud Function Firestore dispatch error', data: { error: err.message } })
261
+ })
262
+ }
263
+ } catch (err) {
264
+ Logger.log({ level: 'error', message: 'Error calling FunctionTriggerDispatcher', data: { error: err.message } })
265
+ }
266
+ }
267
+
268
+ /**
269
+ * Check if change matches client filters
270
+ * @param {Object} change - The change event to check
271
+ * @param {Object} filters - { projectId?, collectionPath? }
272
+ * @returns {boolean}
273
+ */
274
+ matchesFilters (change, filters) {
275
+ // No filters = receive all changes
276
+ if (!filters || Object.keys(filters).length === 0) {
277
+ return true
278
+ }
279
+
280
+ // Project filter
281
+ if (filters.projectId && change.projectId !== filters.projectId) {
282
+ return false
283
+ }
284
+
285
+ // Collection filter
286
+ if (filters.collectionPath && change.collectionPath !== filters.collectionPath) {
287
+ return false
288
+ }
289
+
290
+ return true
291
+ }
292
+
293
+ /**
294
+ * Send SSE formatted message to client
295
+ * @param {Object} ctx - Koa context
296
+ * @param {string} event - Event name
297
+ * @param {Object} data - Data to send
298
+ */
299
+ sendSSE (ctx, event, data) {
300
+ const message = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`
301
+ ctx.res.write(message)
302
+ }
303
+
304
+ /**
305
+ * Start the shared heartbeat interval (one for all clients)
306
+ */
307
+ startHeartbeat () {
308
+ if (this.heartbeatId) return
309
+ this.heartbeatId = setInterval(() => this.sendHeartbeat(), this.heartbeatIntervalMs)
310
+ }
311
+
312
+ /**
313
+ * Stop the shared heartbeat interval when no clients remain
314
+ */
315
+ stopHeartbeat () {
316
+ if (this.heartbeatId) {
317
+ clearInterval(this.heartbeatId)
318
+ this.heartbeatId = null
319
+ }
320
+ }
321
+
322
+ /**
323
+ * Send heartbeat to all connected clients
324
+ * Used to keep connections alive
325
+ */
326
+ sendHeartbeat () {
327
+ for (const [connectionId, { ctx }] of this.clients) {
328
+ try {
329
+ ctx.res.write(': heartbeat\n\n')
330
+ } catch (error) {
331
+ Logger.log({
332
+ level: 'error',
333
+ message: 'Failed to send heartbeat to Firestore SSE client',
334
+ data: { connectionId, error: error.message }
335
+ })
336
+ this.removeClient(connectionId)
337
+ }
338
+ }
339
+ }
340
+
341
+ /**
342
+ * Get current client and listener statistics
343
+ * @returns {Object}
344
+ */
345
+ getStats () {
346
+ const stats = {
347
+ totalClients: this.clients.size,
348
+ totalListeners: this.listeners.size,
349
+ clientsByCollection: {},
350
+ listenerRefCounts: {}
351
+ }
352
+
353
+ for (const [, { filters }] of this.clients) {
354
+ const key = `${filters.projectId}:${filters.collectionPath}`
355
+ stats.clientsByCollection[key] = (stats.clientsByCollection[key] || 0) + 1
356
+ }
357
+
358
+ for (const [listenerKey, { refCount }] of this.listeners) {
359
+ stats.listenerRefCounts[listenerKey] = refCount
360
+ }
361
+
362
+ return stats
363
+ }
364
+ }
365
+
366
+ // Export singleton instance
367
+ export const FirestoreBroadcaster = new FirestoreBroadcasterClass()