@flowerforce/flowerbase 1.7.4 → 1.7.5-beta.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.
@@ -1,8 +1,8 @@
1
+ import type { ServerResponse } from 'http'
1
2
  import { EJSON, ObjectId } from 'bson'
2
3
  import type { FastifyRequest } from 'fastify'
3
- import { ChangeStream, Document } from 'mongodb';
4
+ import type { Document } from 'mongodb'
4
5
  import { services } from '../../services'
5
- import { StateManager } from '../../state'
6
6
  import { GenerateContext } from '../../utils/context'
7
7
  import { Base64Function, FunctionCallBase64Dto, FunctionCallDto } from './dtos'
8
8
  import { FunctionController } from './interface'
@@ -51,6 +51,44 @@ const isReturnedError = (value: unknown): value is { message: string; name: stri
51
51
  const serializeEjson = (value: unknown) =>
52
52
  JSON.stringify(EJSON.serialize(value, { relaxed: false }))
53
53
 
54
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
55
+ !!value && typeof value === 'object' && !Array.isArray(value)
56
+
57
+ type WatchSubscriber = {
58
+ id: string
59
+ user: Record<string, any>
60
+ response: ServerResponse
61
+ extraFilter?: Document
62
+ }
63
+
64
+ type SharedWatchStream = {
65
+ database: string
66
+ collection: string
67
+ stream: {
68
+ on: (event: 'change' | 'error', listener: (payload: any) => void) => void
69
+ off: (event: 'change' | 'error', listener: (payload: any) => void) => void
70
+ close: () => Promise<void> | void
71
+ }
72
+ subscribers: Map<string, WatchSubscriber>
73
+ }
74
+
75
+ const sharedWatchStreams = new Map<string, SharedWatchStream>()
76
+ let watchSubscriberCounter = 0
77
+
78
+ const parseWatchFilter = (args: unknown): Document | undefined => {
79
+ if (!isRecord(args)) return undefined
80
+ const candidate =
81
+ (isRecord(args.filter) ? args.filter : undefined) ??
82
+ (isRecord(args.query) ? args.query : undefined)
83
+ return candidate ? (candidate as Document) : undefined
84
+ }
85
+
86
+ const isReadableDocumentResult = (value: unknown) =>
87
+ !!value &&
88
+ typeof value === 'object' &&
89
+ !Array.isArray(value) &&
90
+ Object.keys(value as Record<string, unknown>).length > 0
91
+
54
92
  /**
55
93
  * > Creates a pre handler for every query
56
94
  * @param app -> the fastify instance
@@ -63,8 +101,6 @@ export const functionsController: FunctionController = async (
63
101
  ) => {
64
102
  app.addHook('preHandler', app.jwtAuthentication)
65
103
 
66
- const streams = {} as Record<string, ChangeStream<Document, Document>>
67
-
68
104
  app.post<{ Body: FunctionCallDto }>('/call', {
69
105
  schema: {
70
106
  tags: ['Functions']
@@ -170,13 +206,12 @@ export const functionsController: FunctionController = async (
170
206
  }
171
207
  const { baas_request, stitch_request } = query
172
208
 
173
- const config: Base64Function = JSON.parse(
209
+ const decodedConfig = JSON.parse(
174
210
  Buffer.from(baas_request || stitch_request || '', 'base64').toString('utf8')
175
211
  )
212
+ const config = EJSON.deserialize(decodedConfig) as Base64Function
176
213
 
177
- const [{ database, collection }] = config.arguments
178
- const app = StateManager.select('app')
179
- const services = StateManager.select('services')
214
+ const [{ database, collection, ...watchArgs }] = config.arguments
180
215
 
181
216
  const headers = {
182
217
  'Content-Type': 'text/event-stream',
@@ -190,36 +225,119 @@ export const functionsController: FunctionController = async (
190
225
  res.raw.writeHead(200, headers)
191
226
  res.raw.flushHeaders();
192
227
 
193
- const requestKey = baas_request || stitch_request
228
+ const streamKey = `${database}::${collection}`
229
+ const subscriberId = `${Date.now()}-${watchSubscriberCounter++}`
230
+ const extraFilter = parseWatchFilter(watchArgs)
231
+ const mongoClient = app.mongo.client as unknown as {
232
+ db: (name: string) => { collection: (name: string) => { watch: (...args: any[]) => any } }
233
+ }
194
234
 
195
- if (!requestKey) return
235
+ let hub = sharedWatchStreams.get(streamKey)
236
+ if (!hub) {
237
+ const stream = mongoClient.db(database).collection(collection).watch([], {
238
+ fullDocument: 'whenAvailable'
239
+ })
240
+ hub = {
241
+ database,
242
+ collection,
243
+ stream,
244
+ subscribers: new Map<string, WatchSubscriber>()
245
+ }
246
+ sharedWatchStreams.set(streamKey, hub)
247
+ }
196
248
 
197
- const changeStream = streams[requestKey]
249
+ const ensureHubListeners = (currentHub: SharedWatchStream) => {
250
+ if ((currentHub as SharedWatchStream & { listenersBound?: boolean }).listenersBound) {
251
+ return
252
+ }
198
253
 
199
- if (changeStream) {
200
- changeStream.on('change', (change) => {
201
- res.raw.write(`data: ${serializeEjson(change)}\n\n`);
202
- });
254
+ const closeHub = async () => {
255
+ currentHub.stream.off('change', onHubChange)
256
+ currentHub.stream.off('error', onHubError)
257
+ sharedWatchStreams.delete(streamKey)
258
+ try {
259
+ await currentHub.stream.close()
260
+ } catch {
261
+ // Ignore stream close errors.
262
+ }
263
+ }
264
+
265
+ const onHubChange = async (change: Document) => {
266
+ const subscribers = Array.from(currentHub.subscribers.values())
267
+ await Promise.all(subscribers.map(async (subscriber) => {
268
+ const subscriberRes = subscriber.response
269
+ if (subscriberRes.writableEnded || subscriberRes.destroyed) {
270
+ currentHub.subscribers.delete(subscriber.id)
271
+ return
272
+ }
273
+
274
+ const docId =
275
+ (change as { documentKey?: { _id?: unknown } })?.documentKey?._id ??
276
+ (change as { fullDocument?: { _id?: unknown } })?.fullDocument?._id
277
+ if (typeof docId === 'undefined') return
278
+
279
+ const readQuery = subscriber.extraFilter
280
+ ? ({ $and: [subscriber.extraFilter, { _id: docId }] } as Document)
281
+ : ({ _id: docId } as Document)
282
+
283
+ try {
284
+ const readableDoc = await services['mongodb-atlas'](app, {
285
+ user: subscriber.user,
286
+ rules
287
+ })
288
+ .db(currentHub.database)
289
+ .collection(currentHub.collection)
290
+ .findOne(readQuery)
291
+
292
+ if (!isReadableDocumentResult(readableDoc)) return
293
+ subscriberRes.write(`data: ${serializeEjson(change)}\n\n`)
294
+ } catch (error) {
295
+ subscriberRes.write(`event: error\ndata: ${formatFunctionExecutionError(error)}\n\n`)
296
+ subscriberRes.end()
297
+ currentHub.subscribers.delete(subscriber.id)
298
+ }
299
+ }))
300
+
301
+ if (!currentHub.subscribers.size) {
302
+ await closeHub()
303
+ }
304
+ }
203
305
 
204
- req.raw.on('close', () => {
205
- console.log("change stream closed");
206
- changeStream?.close?.();
207
- delete streams[requestKey]
208
- });
209
- return
306
+ const onHubError = async (error: unknown) => {
307
+ for (const subscriber of currentHub.subscribers.values()) {
308
+ const subscriberRes = subscriber.response
309
+ if (!subscriberRes.writableEnded && !subscriberRes.destroyed) {
310
+ subscriberRes.write(`event: error\ndata: ${formatFunctionExecutionError(error)}\n\n`)
311
+ subscriberRes.end()
312
+ }
313
+ }
314
+ currentHub.subscribers.clear()
315
+ await closeHub()
316
+ }
317
+
318
+ currentHub.stream.on('change', onHubChange)
319
+ currentHub.stream.on('error', onHubError)
320
+ ; (currentHub as SharedWatchStream & { listenersBound?: boolean }).listenersBound = true
210
321
  }
211
322
 
212
- streams[requestKey] = await services['mongodb-atlas'](app, {
213
- user,
214
- rules
215
- })
216
- .db(database)
217
- .collection(collection)
218
- .watch([], { fullDocument: 'whenAvailable' });
323
+ ensureHubListeners(hub)
219
324
 
325
+ const subscriber: WatchSubscriber = {
326
+ id: subscriberId,
327
+ user,
328
+ response: res.raw,
329
+ extraFilter
330
+ }
331
+ hub.subscribers.set(subscriberId, subscriber)
220
332
 
221
- streams[requestKey].on('change', (change) => {
222
- res.raw.write(`data: ${serializeEjson(change)}\n\n`);
223
- });
333
+ req.raw.on('close', () => {
334
+ const currentHub = sharedWatchStreams.get(streamKey)
335
+ if (!currentHub) return
336
+ currentHub.subscribers.delete(subscriberId)
337
+ if (!currentHub.subscribers.size) {
338
+ void currentHub.stream.close()
339
+ sharedWatchStreams.delete(streamKey)
340
+ }
341
+ })
224
342
  })
225
343
  }