@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,456 @@
1
+ import { v4 as uuid } from 'uuid'
2
+ import cron from 'node-cron'
3
+ import { SqliteStore } from './SqliteStore.js'
4
+ import { FunctionsService } from './FunctionsService.js'
5
+ import { Logger } from './Logger.js'
6
+ import { CLOUD_FUNCTIONS, CLOUD_FUNCTION_INVOCATIONS } from '../db/Tables.js'
7
+ import { FunctionTriggerTypes } from '../enums/FunctionTriggerTypes.js'
8
+
9
+ // Lazy import to avoid circular dependency (FirestoreBroadcaster → FunctionTriggerDispatcher → FirestoreBroadcaster)
10
+ let _firestoreBroadcaster = null
11
+ async function getFirestoreBroadcaster () {
12
+ if (!_firestoreBroadcaster) {
13
+ const mod = await import('./FirestoreBroadcaster.js')
14
+ _firestoreBroadcaster = mod.FirestoreBroadcaster
15
+ }
16
+ return _firestoreBroadcaster
17
+ }
18
+
19
+ /**
20
+ * Event router singleton. Hooks into event sources (Pub/Sub, Firestore, Scheduler)
21
+ * and forwards CloudEvents to matching Cloud Functions.
22
+ */
23
+ class FunctionTriggerDispatcherClass {
24
+ constructor () {
25
+ this.cronJobs = new Map()
26
+ this.isRunning = false
27
+ }
28
+
29
+ async start () {
30
+ const schedulerFunctions = SqliteStore.find(CLOUD_FUNCTIONS, {
31
+ triggerType: FunctionTriggerTypes.SCHEDULER,
32
+ enabled: 1
33
+ })
34
+ for (const fn of schedulerFunctions) {
35
+ try {
36
+ this.startCronJob(fn)
37
+ } catch (error) {
38
+ Logger.log({
39
+ level: 'error',
40
+ message: `Failed to start cron job for function: ${fn.name}`,
41
+ data: { error: error.message }
42
+ })
43
+ }
44
+ }
45
+ const firestoreFunctions = SqliteStore.find(CLOUD_FUNCTIONS, {
46
+ triggerType: FunctionTriggerTypes.FIRESTORE,
47
+ enabled: 1
48
+ })
49
+ for (const fn of firestoreFunctions) {
50
+ try {
51
+ this.startFirestoreListener(fn)
52
+ } catch (error) {
53
+ Logger.log({
54
+ level: 'error',
55
+ message: `Failed to start Firestore listener for function: ${fn.name}`,
56
+ data: { error: error.message }
57
+ })
58
+ }
59
+ }
60
+ this.isRunning = true
61
+ Logger.log({
62
+ level: 'info',
63
+ message: 'FunctionTriggerDispatcher started',
64
+ data: { cronJobs: this.cronJobs.size, firestoreListeners: firestoreFunctions.length }
65
+ })
66
+ }
67
+
68
+ stop () {
69
+ for (const [, task] of this.cronJobs) {
70
+ task.stop()
71
+ }
72
+ this.cronJobs.clear()
73
+ this.isRunning = false
74
+ Logger.log({ level: 'info', message: 'FunctionTriggerDispatcher stopped' })
75
+ }
76
+
77
+ registerFunction (fn) {
78
+ if (fn.triggerType === FunctionTriggerTypes.SCHEDULER) {
79
+ this.startCronJob(fn)
80
+ }
81
+ if (fn.triggerType === FunctionTriggerTypes.FIRESTORE) {
82
+ this.startFirestoreListener(fn)
83
+ }
84
+ }
85
+
86
+ unregisterFunction (fn) {
87
+ if (fn.triggerType === FunctionTriggerTypes.SCHEDULER) {
88
+ this.stopCronJob(fn.id)
89
+ }
90
+ if (fn.triggerType === FunctionTriggerTypes.FIRESTORE) {
91
+ this.stopFirestoreListener(fn)
92
+ }
93
+ }
94
+
95
+ // --- Event Handlers ---
96
+
97
+ async onPubSubMessage (message, topicFullName) {
98
+ if (!this.isRunning) return
99
+ try {
100
+ const functions = SqliteStore.find(CLOUD_FUNCTIONS, {
101
+ triggerType: FunctionTriggerTypes.PUBSUB,
102
+ enabled: 1
103
+ })
104
+ const shortName = topicFullName.split('/').pop()
105
+ for (const fn of functions) {
106
+ const config = typeof fn.triggerConfig === 'string'
107
+ ? JSON.parse(fn.triggerConfig)
108
+ : fn.triggerConfig || {}
109
+ if (!config.topic) continue
110
+ if (config.topic !== topicFullName && config.topic !== shortName) continue
111
+ const cloudEvent = this.buildPubSubCloudEvent(fn, message, topicFullName)
112
+ this.invokeFunction(fn, cloudEvent, `pubsub:${topicFullName}`).catch(err => {
113
+ Logger.log({
114
+ level: 'error',
115
+ message: `Pub/Sub invocation failed for ${fn.name}`,
116
+ data: { error: err.message }
117
+ })
118
+ })
119
+ }
120
+ } catch (error) {
121
+ Logger.log({
122
+ level: 'error',
123
+ message: 'Error processing Pub/Sub message for Cloud Functions',
124
+ data: { error: error.message, topic: topicFullName }
125
+ })
126
+ }
127
+ }
128
+
129
+ async onFirestoreChange (changeEvent) {
130
+ if (!this.isRunning) return
131
+ try {
132
+ const functions = SqliteStore.find(CLOUD_FUNCTIONS, {
133
+ triggerType: FunctionTriggerTypes.FIRESTORE,
134
+ enabled: 1
135
+ })
136
+ for (const fn of functions) {
137
+ const config = typeof fn.triggerConfig === 'string'
138
+ ? JSON.parse(fn.triggerConfig)
139
+ : fn.triggerConfig || {}
140
+ if (!this.matchesFirestoreTrigger(config, changeEvent)) continue
141
+ const cloudEvent = this.buildFirestoreCloudEvent(fn, changeEvent)
142
+ this.invokeFunction(fn, cloudEvent, `firestore:${changeEvent.collectionPath}/${changeEvent.documentId}`).catch(err => {
143
+ Logger.log({
144
+ level: 'error',
145
+ message: `Firestore invocation failed for ${fn.name}`,
146
+ data: { error: err.message }
147
+ })
148
+ })
149
+ }
150
+ } catch (error) {
151
+ Logger.log({
152
+ level: 'error',
153
+ message: 'Error processing Firestore change for Cloud Functions',
154
+ data: { error: error.message }
155
+ })
156
+ }
157
+ }
158
+
159
+ // --- CloudEvent Builders ---
160
+
161
+ buildPubSubCloudEvent (fn, message, topic) {
162
+ return {
163
+ specversion: '1.0',
164
+ id: uuid(),
165
+ type: 'google.cloud.pubsub.topic.v1.messagePublished',
166
+ source: `//pubsub.googleapis.com/${topic}`,
167
+ time: new Date().toISOString(),
168
+ data: {
169
+ message: {
170
+ messageId: message.messageId,
171
+ publishTime: message.publishTime || new Date().toISOString(),
172
+ data: message.data || '',
173
+ attributes: message.attributes || {}
174
+ },
175
+ subscription: `projects/dev-tools/subscriptions/${fn.name}-sub`
176
+ }
177
+ }
178
+ }
179
+
180
+ buildFirestoreCloudEvent (fn, changeEvent) {
181
+ const changeTypeMap = {
182
+ added: 'google.cloud.firestore.document.v1.created',
183
+ modified: 'google.cloud.firestore.document.v1.updated',
184
+ removed: 'google.cloud.firestore.document.v1.deleted'
185
+ }
186
+ const config = typeof fn.triggerConfig === 'string'
187
+ ? JSON.parse(fn.triggerConfig)
188
+ : fn.triggerConfig || {}
189
+ let eventType = changeTypeMap[changeEvent.changeType]
190
+ if (config.eventType === 'write') {
191
+ eventType = 'google.cloud.firestore.document.v1.written'
192
+ }
193
+ const projectId = changeEvent.projectId || 'dev-tools'
194
+ return {
195
+ specversion: '1.0',
196
+ id: uuid(),
197
+ type: eventType,
198
+ source: `//firestore.googleapis.com/projects/${projectId}/databases/(default)`,
199
+ time: changeEvent.timestamp || new Date().toISOString(),
200
+ data: {
201
+ value: changeEvent.document || null,
202
+ oldValue: null,
203
+ updateMask: null
204
+ }
205
+ }
206
+ }
207
+
208
+ buildSchedulerCloudEvent (fn) {
209
+ const config = typeof fn.triggerConfig === 'string'
210
+ ? JSON.parse(fn.triggerConfig)
211
+ : fn.triggerConfig || {}
212
+ return {
213
+ specversion: '1.0',
214
+ id: uuid(),
215
+ type: 'google.cloud.scheduler.job.v1.executed',
216
+ source: `//cloudscheduler.googleapis.com/projects/dev-tools/locations/local/jobs/${fn.name}`,
217
+ time: new Date().toISOString(),
218
+ data: config.payload || {}
219
+ }
220
+ }
221
+
222
+ buildManualCloudEvent (payload) {
223
+ return {
224
+ specversion: '1.0',
225
+ id: uuid(),
226
+ type: 'dev-tools.manual.invoke',
227
+ source: '//dev-tools/manual',
228
+ time: new Date().toISOString(),
229
+ data: payload || {}
230
+ }
231
+ }
232
+
233
+ // --- Core Invocation ---
234
+
235
+ async invokeFunction (fn, cloudEvent, triggerSource) {
236
+ const endpointUrl = FunctionsService.getEndpointUrl(fn.name)
237
+ if (!endpointUrl) {
238
+ Logger.log({
239
+ level: 'warn',
240
+ message: `Function not running, skipping invocation: ${fn.name}`,
241
+ data: { triggerSource }
242
+ })
243
+ return
244
+ }
245
+ const startedAt = new Date().toISOString()
246
+ const invocationId = uuid()
247
+ let responseStatus = null
248
+ let responseBody = null
249
+ let responseTimeMs = null
250
+ let errorText = null
251
+ const startMs = Date.now()
252
+ try {
253
+ const timeoutMs = (fn.timeoutSeconds || 60) * 1000
254
+ const response = await fetch(endpointUrl, {
255
+ method: 'POST',
256
+ headers: {
257
+ 'Content-Type': 'application/cloudevents+json',
258
+ 'ce-id': cloudEvent.id,
259
+ 'ce-type': cloudEvent.type,
260
+ 'ce-source': cloudEvent.source,
261
+ 'ce-specversion': cloudEvent.specversion,
262
+ 'ce-time': cloudEvent.time
263
+ },
264
+ body: JSON.stringify(cloudEvent),
265
+ signal: AbortSignal.timeout(timeoutMs)
266
+ })
267
+ responseTimeMs = Date.now() - startMs
268
+ responseStatus = response.status
269
+ try {
270
+ responseBody = await response.text()
271
+ } catch {
272
+ responseBody = null
273
+ }
274
+ } catch (error) {
275
+ responseTimeMs = Date.now() - startMs
276
+ errorText = error.message
277
+ Logger.log({
278
+ level: 'error',
279
+ message: `Function invocation failed: ${fn.name}`,
280
+ data: { error: error.message, triggerSource }
281
+ })
282
+ }
283
+ // Record invocation
284
+ try {
285
+ SqliteStore.create(CLOUD_FUNCTION_INVOCATIONS, {
286
+ id: invocationId,
287
+ functionId: fn.id,
288
+ functionName: fn.name,
289
+ triggerType: fn.triggerType,
290
+ triggerSource: triggerSource || null,
291
+ cloudEvent: JSON.stringify(cloudEvent),
292
+ responseStatus,
293
+ responseBody,
294
+ responseTimeMs,
295
+ error: errorText,
296
+ startedAt,
297
+ completedAt: new Date().toISOString()
298
+ })
299
+ } catch (error) {
300
+ Logger.log({
301
+ level: 'warn',
302
+ message: 'Failed to record function invocation',
303
+ data: { functionName: fn.name, error: error.message }
304
+ })
305
+ }
306
+ // Update function stats
307
+ try {
308
+ SqliteStore.update(CLOUD_FUNCTIONS, fn.id, {
309
+ invocationCount: (fn.invocationCount || 0) + 1,
310
+ lastInvokedAt: new Date().toISOString(),
311
+ lastStatus: responseStatus
312
+ })
313
+ } catch (error) {
314
+ Logger.log({
315
+ level: 'warn',
316
+ message: 'Failed to update function stats',
317
+ data: { functionName: fn.name, error: error.message }
318
+ })
319
+ }
320
+ // Broadcast invocation to SSE clients
321
+ const invocation = {
322
+ id: invocationId,
323
+ functionId: fn.id,
324
+ functionName: fn.name,
325
+ triggerType: fn.triggerType,
326
+ triggerSource,
327
+ responseStatus,
328
+ responseTimeMs,
329
+ error: errorText,
330
+ startedAt,
331
+ completedAt: new Date().toISOString()
332
+ }
333
+ FunctionsService.broadcastInvocation(invocation)
334
+ return invocation
335
+ }
336
+
337
+ // --- Cron Methods ---
338
+
339
+ startCronJob (fn) {
340
+ const config = typeof fn.triggerConfig === 'string'
341
+ ? JSON.parse(fn.triggerConfig)
342
+ : fn.triggerConfig || {}
343
+ const cronExpression = config.schedule || config.cronExpression
344
+ if (!cronExpression) {
345
+ Logger.log({
346
+ level: 'warn',
347
+ message: `No cron expression for scheduler function: ${fn.name}`
348
+ })
349
+ return
350
+ }
351
+ if (!cron.validate(cronExpression)) {
352
+ Logger.log({
353
+ level: 'error',
354
+ message: `Invalid cron expression for function: ${fn.name}`,
355
+ data: { cronExpression }
356
+ })
357
+ return
358
+ }
359
+ if (this.cronJobs.has(fn.id)) {
360
+ this.stopCronJob(fn.id)
361
+ }
362
+ const task = cron.schedule(cronExpression, () => {
363
+ const cloudEvent = this.buildSchedulerCloudEvent(fn)
364
+ this.invokeFunction(fn, cloudEvent, `scheduler:${cronExpression}`).catch(err => {
365
+ Logger.log({
366
+ level: 'error',
367
+ message: `Scheduled invocation failed for ${fn.name}`,
368
+ data: { error: err.message }
369
+ })
370
+ })
371
+ })
372
+ this.cronJobs.set(fn.id, task)
373
+ Logger.log({
374
+ level: 'info',
375
+ message: `Cron job started for function: ${fn.name}`,
376
+ data: { cronExpression }
377
+ })
378
+ }
379
+
380
+ stopCronJob (functionId) {
381
+ const task = this.cronJobs.get(functionId)
382
+ if (task) {
383
+ task.stop()
384
+ this.cronJobs.delete(functionId)
385
+ }
386
+ }
387
+
388
+ // --- Firestore Listener Methods ---
389
+
390
+ async startFirestoreListener (fn) {
391
+ const config = typeof fn.triggerConfig === 'string'
392
+ ? JSON.parse(fn.triggerConfig)
393
+ : fn.triggerConfig || {}
394
+ if (!config.collectionPath) {
395
+ Logger.log({
396
+ level: 'warn',
397
+ message: `No collectionPath for Firestore function: ${fn.name}`
398
+ })
399
+ return
400
+ }
401
+ const projectId = config.projectId || 'tipi-development'
402
+ try {
403
+ const broadcaster = await getFirestoreBroadcaster()
404
+ broadcaster.startCollectionListener(projectId, config.collectionPath)
405
+ Logger.log({
406
+ level: 'info',
407
+ message: `Firestore listener started for function: ${fn.name}`,
408
+ data: { projectId, collectionPath: config.collectionPath }
409
+ })
410
+ } catch (error) {
411
+ Logger.log({
412
+ level: 'error',
413
+ message: `Failed to start Firestore listener for function: ${fn.name}`,
414
+ data: { error: error.message }
415
+ })
416
+ }
417
+ }
418
+
419
+ async stopFirestoreListener (fn) {
420
+ const config = typeof fn.triggerConfig === 'string'
421
+ ? JSON.parse(fn.triggerConfig)
422
+ : fn.triggerConfig || {}
423
+ if (!config.collectionPath) return
424
+ const projectId = config.projectId || 'tipi-development'
425
+ try {
426
+ const broadcaster = await getFirestoreBroadcaster()
427
+ broadcaster.stopCollectionListener(projectId, config.collectionPath)
428
+ } catch (error) {
429
+ Logger.log({
430
+ level: 'warn',
431
+ message: `Failed to stop Firestore listener for function: ${fn.name}`,
432
+ data: { error: error.message }
433
+ })
434
+ }
435
+ }
436
+
437
+ // --- Helpers ---
438
+
439
+ matchesFirestoreTrigger (config, changeEvent) {
440
+ if (!config.collectionPath) return false
441
+ if (config.collectionPath !== changeEvent.collectionPath) return false
442
+ if (config.projectId && config.projectId !== changeEvent.projectId) return false
443
+ if (config.eventType && config.eventType !== 'write') {
444
+ const eventTypeMap = {
445
+ created: 'added',
446
+ updated: 'modified',
447
+ deleted: 'removed'
448
+ }
449
+ const expectedChangeType = eventTypeMap[config.eventType]
450
+ if (expectedChangeType && expectedChangeType !== changeEvent.changeType) return false
451
+ }
452
+ return true
453
+ }
454
+ }
455
+
456
+ export const FunctionTriggerDispatcher = new FunctionTriggerDispatcherClass()