corebasic 1.0.231 → 1.0.233

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/libs/features.ts CHANGED
@@ -8,11 +8,90 @@ import axios from 'axios'
8
8
  // @ts-ignore
9
9
  import jwt from 'jsonwebtoken'
10
10
 
11
- export type Message = unknown
12
- export type Req = unknown
13
- export type Res = unknown
11
+
12
+ // ===================================
13
+
14
+ type HttpMethod = "get" | "post" | "put" | "delete" | "patch" | "head" | "options" | "connect" | "trace"
15
+
16
+
17
+ type FeatureMeta = {
18
+ feature: string,
19
+ app: string,
20
+ user: string,
21
+ outlet: string,
22
+ company: string,
23
+ staff: string,
24
+ client: string,
25
+ version: string,
26
+ txn: string,
27
+ // invoiceTxn?: string // TODO: Must also include invoiceTxn
28
+ }
29
+ type FeatureParams = Record<string, string>
30
+ type FeatureMessage = {
31
+ meta: FeatureMeta
32
+ data: unknown
33
+ params: FeatureParams
34
+ // To be removed 1
35
+ date: number // used as message.date
36
+ // To be removed 2
37
+ topic: string // used as req.body.topic
38
+ feature: string // used as req.body.feature or message.feature
39
+ user: string // used as req.body.user or message.user
40
+ txn: string // used as message.txn
41
+
42
+
43
+ // client // Only used as req.body.client in auth.ts, session.ts and messaging.ts
44
+ // version // Only used as req.body.version in auth.ts and session.ts
45
+ // outlet, company // Only used as req.body.outlet, req.body.company in session.ts
46
+ // app // Only used as req.body.app in auth.ts and session.ts
47
+ }
48
+ type PayloadMessage = FeatureMeta & {
49
+ data: unknown
50
+ params?: FeatureParams
51
+ }
52
+
53
+ // =========
54
+
55
+ export type Req = {
56
+ meta: FeatureMeta
57
+ body: FeatureMessage
58
+ params: FeatureParams
59
+ method: string
60
+ path: string
61
+ url: string
62
+ headers: Record<string, string | string[] | undefined>
63
+ query: Record<string, unknown>
64
+ on: (event: string, callback: () => void) => void
65
+ }
66
+
67
+ type ResJson = (response: unknown) => void
68
+ type ResSend = (response: unknown) => void;
69
+ type ResStatus = (status: number) => {send: ResSend, json: ResJson};
70
+ export type Res = {
71
+ send: ResSend
72
+ json: ResJson
73
+ end: (finalData?: string) => void
74
+ status: ResStatus
75
+ sendStatus: (status: number) => void
76
+ setHeader: (header: string, value: string) => void
77
+ flushHeaders: () => void
78
+ headersSent: boolean
79
+ writableEnded: boolean
80
+ }
81
+
82
+
83
+ // ========= Express Types ==========
84
+ type ExpressRequest = Omit<Req, "meta" | "body"> & { body: PayloadMessage }
85
+ type ExpressResponse = Res
86
+ type ExpressHandler = (req: ExpressRequest, res: ExpressResponse) => Promise<void>
87
+ type ExpressApplication = { [Method in HttpMethod]: (path: string, handler: ExpressHandler) => unknown }
88
+ // ===================================
89
+
90
+
91
+ // export type Req = unknown
92
+ // export type Res = unknown
14
93
  export type Query = (req: Req, res: Res) => Promise<void>
15
- export type Command = (topic: string, message: Message, req?: Req, res?: Res) => Promise<void>
94
+ export type Command = (topic: string, message: FeatureMessage, req?: Req, res?: Res) => Promise<void>
16
95
  type Handler = Query | Command
17
96
 
18
97
 
@@ -28,11 +107,13 @@ type FeatureEntry = { api: string, topic: string, bypass: boolean, featureless:
28
107
 
29
108
 
30
109
  let features: Record<string, FeatureEntry> = {}
31
- let apis = {}
110
+ let apis: Record<string, Record<string, FeatureEntry>> = {
111
+ // <api>: {<feature>: FeatureEntry}
112
+ }
32
113
 
33
- const getFeatureMethod = api => api.split(' ')[0].toLowerCase()
34
- const getFeatureUrl = api => api.split(' ')[1]
35
- const getFeature = name => features[name]
114
+ const getFeatureMethod = (api: string): HttpMethod => api.split(' ')[0]!.toLowerCase() as HttpMethod
115
+ const getFeatureUrl = (api: string): string => { const url = api.split(' ')[1]; if (!url) throw new Error(`Invalid Feature url in api: ${api}`); return url}
116
+ const getFeature = (name: string): FeatureEntry | undefined => features[name]
36
117
 
37
118
  const DEPLOY_TOKEN_SECRET = process.env.DEPLOY_TOKEN_SECRET || "MY_SECRET_DEPLOY_TOKEN"
38
119
  const SERVICE_ACCESS_TOKEN = jwt.sign({app: process.env.APP_DEPLOYMENT_NAME}, DEPLOY_TOKEN_SECRET, { expiresIn: '365d' });
@@ -63,39 +144,50 @@ let appId = Utils.uid()
63
144
  let SERVICE_ADDRESS = process.env.APP_ENDPOINT || 'http://127.0.0.1:3000'
64
145
 
65
146
 
66
- export const get = feature => {
147
+ export const get = (feature: string) => {
67
148
  const throwError = () => {throw new Error(`Feature ${feature} not found in internal or external list during Features.subscribe call`)}
68
- const baseFeature: FeatureEntry = getFeature(feature)
149
+ const baseFeature: FeatureEntry | undefined = getFeature(feature)
69
150
  let {api, service = SERVICE_ADDRESS, topic} = baseFeature ? baseFeature as FeatureEntry & {service: string} : (SLYP_FEATURES_LIST[feature] ?? throwError())
70
151
  return {api, service, topic}
71
152
  }
72
153
 
73
- export const send = async (meta, feature, data, params) => {
154
+ export const send = async (meta: Partial<FeatureMeta>, feature: string, data?: unknown, params?: FeatureParams) => {
74
155
  const throwError = () => {throw new Error(`Feature ${feature} not found in internal or external list during inter feature call`)}
75
- const baseFeature: FeatureEntry = getFeature(feature)
156
+ const baseFeature: FeatureEntry | undefined = getFeature(feature)
76
157
  let {api, service = SERVICE_ADDRESS} = baseFeature ? baseFeature as FeatureEntry & {service: string} : (SLYP_FEATURES_LIST[feature] ?? throwError())
77
158
  let method = getFeatureMethod(api)
78
159
  let url = getFeatureUrl(api)
79
160
 
80
161
  if (params || url.includes(':')) {
81
162
  for (let key in params)
82
- url = url.replace(`:${key}`, params[key])
163
+ url = url.replace(`:${key}`, params[key]!)
83
164
  if (url.includes(':'))
84
165
  throw new Error(`Error: Internal feature call send on feature: ${feature} when api/params/substitution`)
85
166
  }
86
167
 
87
- let payload = { ...meta, data, feature, txn: Utils.uid() }
168
+ const payload: PayloadMessage = { ...meta, data, feature, txn: Utils.uid() } as PayloadMessage
88
169
  service = service.replace('http://slyp.app', 'https://slyp.app')
89
170
 
90
171
  // return (await axios[method](`${service}${url}`, { data: payload, headers: {jwt: headers.jwt}, timeout: 1000 })).data
91
172
 
92
173
  // Used to work. But request to 127.0.0.1 fails to send the specified headers along with the request.
93
174
  // return (await axios[method](`${service}${url}`, payload, {headers: {jwt: SERVICE_ACCESS_TOKEN, service: true}, timeout: 1000 })).data // Worked Earlier, but issue spotted
94
- if (getFeature(feature)) { // Local call
175
+ if (baseFeature) { // Local call
95
176
  let response;
96
- let req = {meta,body: payload, params,method}
177
+ let req: ExpressRequest = { body: payload, params: params ?? {}, method, path: url, url: '', headers: {}, query: {}, on: (_event: string, _callback:()=>void) => {} }
97
178
  req = JSON.parse(JSON.stringify(req))
98
- let res = {json: payload => response = payload, end: () => true }
179
+ const callback = (payload: unknown): void => { response = payload }
180
+ const res: ExpressResponse = {
181
+ json: callback,
182
+ send: callback,
183
+ end: () => {},
184
+ status: (_status: number) => { return {send: callback, json: callback} },
185
+ sendStatus: (_status: number) => {},
186
+ setHeader: (_header: string, _value: string) => {},
187
+ flushHeaders: () => {},
188
+ headersSent: false,
189
+ writableEnded: false,
190
+ }
99
191
  await apiHandler(req, res);
100
192
  return response
101
193
  }
@@ -109,13 +201,13 @@ export const send = async (meta, feature, data, params) => {
109
201
  })).data;
110
202
  }
111
203
 
112
- let appids = {}
204
+ let appids: Record<string, boolean> = {}
113
205
  async function announce() {
114
206
  let exp_features: Record<string, RemoteFeatureEntry> = {}
115
207
  for (let [key, {api,subscribe}] of Object.entries(features))
116
208
  exp_features[key] = {api, service: SERVICE_ADDRESS, subscribe, topic: ""}
117
209
 
118
- await Messaging.subscribe(`${process.env.REDIS_CHANNEL_PREFIX}_SLYP_FEATURES_LIST`, async (message, channel) => {
210
+ await Messaging.subscribe(`${process.env.REDIS_CHANNEL_PREFIX}_SLYP_FEATURES_LIST`, async (message, _channel) => {
119
211
  let {uid, ...msg} = JSON.parse(message) as Record<string, RemoteFeatureEntry> & {uid: string}
120
212
  if (uid !== appId && !appids[uid]) {
121
213
  appids[uid] = true
@@ -128,17 +220,19 @@ async function announce() {
128
220
  }
129
221
 
130
222
 
131
- function getFeaturelessFeature(req) {
223
+ function getFeaturelessFeature(req: ExpressRequest): FeatureEntry | undefined {
132
224
  if (Session.ALLOWED_URLS.includes(req.path)) {
133
- for(let key in features){
134
- if (features[key].featureless && features[key].api === req.method.toUpperCase() + ' ' + req.path) {
225
+ for(let key in features) {
226
+ const feature = features[key]!
227
+ if (feature.featureless && feature.api === req.method.toUpperCase() + ' ' + req.path) {
135
228
  return getFeature(key)
136
229
  }
137
230
  }
138
231
  }
232
+ return undefined
139
233
  }
140
234
 
141
- const apiHandler = async (req, res) => {
235
+ const apiHandler = async (req: ExpressRequest, res: ExpressResponse) => {
142
236
  let method = req.method.toLowerCase()
143
237
  let featureless
144
238
  try {
@@ -155,50 +249,89 @@ const apiHandler = async (req, res) => {
155
249
  throw { status: 404, message: `Resource not found. Feature ${req.body.feature} not available.` }
156
250
  }
157
251
 
252
+ feature = feature! // typescript validation with !
253
+
158
254
  let topic = feature.topic
159
- let params = getFeatureUrl(feature.api).split("/").filter(item => item.startsWith(":")).map(item => item.replace(":", ""))
255
+ let params = getFeatureUrl(feature.api).split("/").filter((item: string) => item.startsWith(":")).map((item: string) => item.replace(":", ""))
160
256
  for (let param of params)
161
257
  if (!req.params[param])
162
258
  throw { status: 404, message: "Resource not found. One or more url parameter not specified." }
163
259
 
164
- let meta = {...req.body, data: undefined}
165
- req.meta = meta
260
+ let meta = {...req.body, data: undefined} as FeatureMeta // TODO: Must also include invoiceTxn
261
+
166
262
  if (process.env.USE_DEFAULT_COMPANY) {
167
- req.meta.company = 'DEFAULT_COMPANY'
168
- req.meta.outlet = 'DEFAULT_OUTLET'
263
+ meta.company = 'DEFAULT_COMPANY'
264
+ meta.outlet = 'DEFAULT_OUTLET'
169
265
  }
170
266
 
267
+ meta.txn = meta.txn ?? Utils.uid()
268
+
269
+ const message: FeatureMessage = {
270
+ meta,
271
+ data: req.body.data,
272
+ params: req.params,
273
+
274
+ // To be removed 1
275
+ date: new Date().getTime(),
276
+
277
+ // To be removed 2
278
+ topic,
279
+ feature: req.body.feature,
280
+ user: req.body.user,
281
+ txn: meta.txn,
282
+ }
283
+
284
+ const appReq: Req = {
285
+ meta: message.meta,
286
+ body: message,
287
+ params: message.params,
288
+ method: method,
289
+ path: req.path,
290
+ url: req.url,
291
+ headers: req.headers,
292
+ query: req.query,
293
+ on: req.on,
294
+ }
295
+ const appRes: Res = res as Res
296
+
171
297
  if (method === "get") {
172
298
  try {
173
- req.body = {...req.body, topic}
174
- await (feature.handler as Query)(req, res)
175
- } catch (err) {
299
+ // await feature.handler({...req, headers: req.headers, body: {...req.body, topic} }, res)
300
+ // req.body = {...req.body, topic}
301
+ await (feature.handler as Query)(appReq, appRes)
302
+ } catch (err: any) {
176
303
  if (process.env.DEBUG_MODE)
177
- console.log('Error: Feature: ', req.body?.feature ?? (featureless ? `Featureless Api:${featureless.api}` : undefined), err)
304
+ console.error('Error: Feature: ', req.body?.feature ?? (featureless ? `Featureless Api:${featureless.api}` : undefined), err)
178
305
  throw {status: 500, message: "Failed to GET feature", ...err}
179
306
  }
180
307
  } else if (method !== "get" && feature.bypass) {
181
308
  try {
182
- await (feature.handler as Command)(topic, prepareMessage(req), req, res)
183
- } catch (err) {
309
+ await (feature.handler as Command)(topic, message, appReq, appRes)
310
+ } catch (err: any) {
184
311
  if (process.env.DEBUG_MODE)
185
- console.log('Error: Feature: ', req.body?.feature ?? (featureless ? `Featureless Api:${featureless.api}` : undefined), err)
312
+ console.error('Error: Feature: ', req.body?.feature ?? (featureless ? `Featureless Api:${featureless.api}` : undefined), err)
186
313
  throw {status: 500, message: "Failed to POST feature", ...err}
187
314
  }
188
- } else
189
- await commandAction(req, res, topic)
190
- } catch (err) {
315
+ } else {
316
+ try {
317
+ await Kafka.send(`Features.${topic}`, message, message.user, { compression: Kafka.CompressionTypes.GZIP })
318
+ } catch {
319
+ throw {status: 500, message: "Failed to queue the transaction"}
320
+ }
321
+ appRes.json({ data: { txn: message.txn, success: true, status: "Queued", featureQueued: true } })
322
+ }
323
+ } catch (err: any) {
191
324
  if (process.env.DEBUG_MODE)
192
- console.log('Error: Feature: ', req.body?.feature ?? (featureless ? `Featureless Api:${featureless.api}` : undefined), err)
325
+ console.error('Error: Feature: ', req.body?.feature ?? (featureless ? `Featureless Api:${featureless.api}` : undefined), err)
193
326
  try { // Sometimes error occurs when disconnecting stream from front end
194
327
  res.status(err.status ?? 500).json(err)
195
328
  } catch (_) { }
196
329
  }
197
330
  }
198
331
 
199
- let ExpressApp
200
- let PROJECT_ROOT_URL
201
- export async function registerFeatures(newFeatures) {
332
+ let ExpressApp: ExpressApplication
333
+ let PROJECT_ROOT_URL: URL
334
+ export async function registerFeatures(newFeatures: Record<string, FeatureEntry>) {
202
335
  // Registering handlers
203
336
  await registerHandler(newFeatures)
204
337
 
@@ -207,15 +340,15 @@ export async function registerFeatures(newFeatures) {
207
340
  // Registering apis
208
341
  registerApi()
209
342
  }
210
- async function registerHandler(features) {
343
+ async function registerHandler(features: Record<string, FeatureEntry>) {
211
344
  for (let name in features) {
212
- let feature = features[name]
345
+ let feature = features[name]!
213
346
  apis[feature.api] = apis[feature.api] ?? {}
214
- apis[feature.api][name] = feature
347
+ apis[feature.api]![name] = feature
215
348
  const featureName = name
216
349
 
217
350
  const nameParts = name.split('.')
218
- let handler = nameParts.pop()
351
+ let handler = nameParts.pop()!
219
352
  const featurePath = `src/${nameParts.join('/')}`
220
353
 
221
354
  if (featurePath === 'src/transactions/query')
@@ -229,8 +362,10 @@ async function registerHandler(features) {
229
362
  }
230
363
  function registerApi() {
231
364
  for (let api in apis) {
232
- let method = getFeatureMethod(api)
365
+ let method: HttpMethod = getFeatureMethod(api)
233
366
  let url = api.split(' ')[1]
367
+ if (!url)
368
+ throw new Error(`No url path found in api ${api} during Features.registerApi()`)
234
369
  if (api === 'GET /transactions/:id')
235
370
  continue
236
371
 
@@ -238,22 +373,22 @@ function registerApi() {
238
373
  }
239
374
  }
240
375
 
241
- export const start = async (app, url, file) => {
376
+ export const start = async (app: ExpressApplication, url: URL, file: string) => {
242
377
  const globalMeta = Dip.globalMeta()
243
378
  Dip.excludeCollectionConfig("users.txns")
244
379
  Dip.excludeCollectionConfig("Features.txns")
245
380
  ExpressApp = app
246
381
  PROJECT_ROOT_URL = url
247
- features = await Utils.fileToJson(url, file)
382
+ features = await Utils.fileToJson(url, file) as Record<string, FeatureEntry>
248
383
  await announce()
249
384
 
250
- app.get('/features', async (req, res) => {
251
- let exp_features = {}
385
+ app.get('/features', async (_req: ExpressRequest, res: ExpressResponse) => {
386
+ let exp_features: Record<string, RemoteFeatureEntry> = {}
252
387
  for (let [key, {api}] of Object.entries(features))
253
- exp_features[key] = {api, service: `${SERVICE_ADDRESS}`}
388
+ exp_features[key] = {api, service: `${SERVICE_ADDRESS}`, subscribe: '', topic: ''}
254
389
  if (process.env.LOAD_LOCAL_FEATURES) {
255
390
  for (let key in SLYP_FEATURES_LIST)
256
- SLYP_FEATURES_LIST[key].headers = {JWT: SERVICE_ACCESS_TOKEN, service: true, NDCURVE_DEVELOPER_LICENSE_ACCESS_TOKEN}
391
+ SLYP_FEATURES_LIST[key]!.headers = {JWT: SERVICE_ACCESS_TOKEN, service: true, NDCURVE_DEVELOPER_LICENSE_ACCESS_TOKEN} // TODO: CRITICAL: Security issue
257
392
  }
258
393
  res.json({ data: { ...SLYP_FEATURES_LIST, ...exp_features } })
259
394
  })
@@ -262,23 +397,24 @@ export const start = async (app, url, file) => {
262
397
  await registerFeatures(features)
263
398
 
264
399
  // Get unique kafka topics list from features
265
- let kafkaTopics = []
266
- for (let name in features) {
267
- let feature = features[name]
268
- if (feature.api.split(' ')[0].toLowerCase() === "get" )
400
+ let kafkaTopics: string[] = []
401
+ for (const name in features) {
402
+ let feature = features[name]!
403
+ if (feature.api.split(' ')[0]?.toLowerCase() === "get" )
269
404
  continue
270
405
  kafkaTopics = [... new Set(kafkaTopics.concat([feature.topic]))]
271
406
  }
272
407
 
273
408
  // Subscribe to each topic
274
409
  for (let topic of kafkaTopics) {
275
- Kafka.receive(`Features.${topic}`, async (topic, message) => {
410
+ const groupId = ''
411
+ Kafka.receive<FeatureMessage>(`Features.${topic}`, groupId, async (topic: string, message: FeatureMessage) => {
276
412
  if (Utils.isEmpty(message.meta?.company))
277
413
  message.meta = { ...message.meta, company: "GLOBAL", outlet: "GLOBAL" }
278
414
  if (!message?.feature)
279
415
  return
280
416
 
281
- const timer = ms => new Promise(res => setTimeout(res, ms)) // A promise that resolves after "ms" Milliseconds
417
+ const timer = (ms: number) => new Promise(res => setTimeout(res, ms)) // A promise that resolves after "ms" Milliseconds
282
418
  topic = topic.replace(/^.*Features./,'')
283
419
  while (true) {
284
420
  try {
@@ -286,7 +422,12 @@ export const start = async (app, url, file) => {
286
422
  // // TODO: use $useChunks: [] once support is added in dip insert
287
423
  // await Dip.insert(globalMeta, `users.txns`, { _id: `${message.user}_${iso_date}_${message.txn}`, user: message.user, feature: message.feature, date: message.date, created: message.date, updated: message.date, status: "Queued" }, {idempotent: true}) // Can always update the status later
288
424
  // TODO: Avoid duplicate processing
289
- await (features[message.feature].handler as Command)(topic, message)
425
+ const feature = features[message.feature]
426
+ if (!feature) {
427
+ console.warn(`Feature ${message.feature} missing, in executing handler during Kafka.receive(topic: ${topic})`)
428
+ throw new Error(`Feature ${message.feature} missing, in executing handler during Kafka.receive(topic: ${topic})`)
429
+ }
430
+ await (feature.handler as Command)(topic, message)
290
431
  await Dip.insert(globalMeta, "Features.txns", {_id: message.txn, status: "Processed"}, {idempotent: true})
291
432
  break
292
433
  } catch(err) {
@@ -302,9 +443,9 @@ export const start = async (app, url, file) => {
302
443
  await subscribe()
303
444
 
304
445
  // Transaction status api
305
- app.get('/transactions/:id', async (req, res) => { // Front end calls this as a regular feature call. so `req.meta` exists
446
+ app.get('/transactions/:id', async (req: ExpressRequest, res: ExpressResponse) => { // Front end calls this as a regular feature call. so `req.meta` exists
306
447
  try {
307
- let items = await Dip.query(globalMeta, "Features.txns", {_id: req.params.id})
448
+ let items = await Dip.query(globalMeta, "Features.txns", {_id: req.params!.id})
308
449
  if (items.length)
309
450
  res.json({ data: { ...items[0], txn: items._id } })
310
451
  else
@@ -325,70 +466,61 @@ export const start = async (app, url, file) => {
325
466
  // }
326
467
  // })
327
468
 
328
- try { await Messaging.start(app) } catch { console.log('Messaging failed to start. Maybe missing redis') }
469
+ try { await Messaging.start((app as unknown) as Messaging.ExpressApp ) } catch { console.log('Messaging failed to start. Maybe missing redis') }
329
470
  }
330
471
 
331
- function prepareMessage(req) {
332
- let txn = req.body.txn ?? Utils.uid()
333
- let _id = req.body.data?._id ?? txn
334
- let {data, feature, app, user, outlet, company, client, version} = req.body
335
- let meta = req.meta
336
- let params = req.params
337
- return { data, params, meta, feature, app, user, outlet, company, client, version, txn, id: _id, date: new Date().getTime() }
338
- }
339
- const commandAction = async (req, res, topic) => {
340
- let message = prepareMessage(req)
341
- try {
342
- await Kafka.send(`Features.${topic}`, message, message.user, { compression: Kafka.CompressionTypes.GZIP })
343
- } catch {
344
- throw {status: 500, message: "Failed to queue the transaction"}
345
- }
346
- res.json({ data: { txn: message.txn, success: true, status: "Queued", featureQueued: true } })
347
- }
348
472
 
349
473
 
350
474
 
351
- let subscriptions = {
475
+ let subscriptions: Record<string, string> = {
352
476
  // "coins.query.hello": "invoices.command.add", // Consumer : Feature of Topic
353
477
  }
354
- let subscribed_consumers = {
355
- }
478
+ let subscribed_consumers: Record<string, Kafka.KafkaConsumer> = {}
356
479
  async function subscribe() {
357
480
  let Features = {get,send}
358
481
 
359
- for (let [key, {subscribe}] of Object.entries(features))
482
+ for (let [key, {subscribe}] of Object.entries(features)) {
360
483
  if (!Utils.isEmpty(subscribe) && subscriptions[key] !== subscribe && subscribed_consumers[key]) {
361
484
  try { await subscribed_consumers[key].disconnect() } catch (_) {}
362
485
  delete subscribed_consumers[key]
363
486
  }
364
- for (let key in SLYP_FEATURES_LIST)
365
- if (!Utils.isEmpty(SLYP_FEATURES_LIST[key].subscribe) && subscriptions[key] !== SLYP_FEATURES_LIST[key].subscribe && subscribed_consumers[key]) {
487
+ }
488
+ for (let key in SLYP_FEATURES_LIST) {
489
+ const feature = SLYP_FEATURES_LIST[key]
490
+ if (!Utils.isEmpty(feature?.subscribe) && subscriptions[key] !== feature?.subscribe && subscribed_consumers[key]) {
366
491
  try { await subscribed_consumers[key].disconnect() } catch (_) {}
367
492
  delete subscribed_consumers[key]
368
493
  }
494
+ }
369
495
 
370
496
 
371
- for (let key in SLYP_FEATURES_LIST)
372
- if (!Utils.isEmpty(SLYP_FEATURES_LIST[key].subscribe)) {
373
- subscriptions[key] = SLYP_FEATURES_LIST[key].subscribe
497
+ for (let key in SLYP_FEATURES_LIST) {
498
+ const feature = SLYP_FEATURES_LIST[key]
499
+ if (!Utils.isEmpty(feature?.subscribe)) {
500
+ subscriptions[key] = feature!.subscribe
374
501
  }
375
- for (let [key, {subscribe}] of Object.entries(features))
502
+ }
503
+ for (let [key, {subscribe}] of Object.entries(features)) {
376
504
  if (!Utils.isEmpty(subscribe)) {
377
505
  subscriptions[key] = subscribe
378
506
  }
507
+ }
379
508
 
380
509
 
381
510
  for (let consumer in subscriptions) {
382
- let publisher = subscriptions[consumer]
383
- let {topic} = getFeature(publisher)
511
+ let publisher = subscriptions[consumer]!
512
+
513
+ const publisherFeature = getFeature(publisher)
384
514
 
385
- if (subscribed_consumers[consumer] || !getFeature(publisher))
515
+ if (subscribed_consumers[consumer] || !publisherFeature)
386
516
  continue
387
517
 
388
- let kafka_consumer = Kafka.receive(`Features.${topic}`, consumer, async (topic, message) => {
518
+ let {topic} = publisherFeature
519
+
520
+ let kafka_consumer_promise = Kafka.receive<FeatureMessage>(`Features.${topic}`, consumer, async (topic: string, message: FeatureMessage) => {
389
521
  topic = topic.replace(/^.*Features./,'')
390
522
 
391
- const timer = ms => new Promise(res => setTimeout(res, ms)) // A promise that resolves after "ms" Milliseconds
523
+ const timer = (ms: number): Promise<void> => new Promise(res => setTimeout(res, ms)) // A promise that resolves after "ms" Milliseconds
392
524
 
393
525
  while (true) {
394
526
  try {
@@ -400,6 +532,12 @@ async function subscribe() {
400
532
  await timer(10000);
401
533
  }
402
534
  })
403
- subscribed_consumers[consumer] = kafka_consumer
535
+
536
+ kafka_consumer_promise
537
+ .then(kafka_consumer => {
538
+ subscribed_consumers[consumer] = kafka_consumer
539
+ }).catch(() => {
540
+ throw new Error("Error: Remote service to service kafka Feature subscription.")
541
+ })
404
542
  }
405
543
  }
package/libs/kafka.ts CHANGED
@@ -5,10 +5,11 @@ export const logLevel = KafkaLogLevel
5
5
  export const CompressionTypes = KafkaCompressionTypes
6
6
 
7
7
 
8
- type KafkaCallback = (topic: string, message: unknown, partition: number) => unknown | Promise<unknown>
8
+ export type KafkaCallback<T = unknown> = (topic: string, message: T, partition: number) => unknown | Promise<unknown>
9
9
 
10
- type KafkaConsumer = {
10
+ export type KafkaConsumer = {
11
11
  connect: () => Promise<void>
12
+ disconnect: () => Promise<void>
12
13
  subscribe: (arg: {topic: string; fromBeginning: boolean}) => Promise<void>
13
14
  run: (arg: {
14
15
  autoCommit: boolean
@@ -100,27 +101,24 @@ export const createTopic = async (topic: string, partition?: number, replicas?:
100
101
 
101
102
  let consumers = []
102
103
 
103
- const start_consumer = async function (topic: string, groupId: string | KafkaCallback, callback?: KafkaCallback) {
104
- callback = typeof groupId === "string" ? callback : groupId
105
- groupId = typeof groupId === "string" ? `${appName}.${groupId}` : `${appName}.${topic}`
104
+ const start_consumer = async function<T>(topic: string, groupId: string, callback: KafkaCallback<T>): Promise<KafkaConsumer> {
105
+ groupId = groupId ? `${appName}.${groupId}` : `${appName}.${topic}`
106
106
 
107
- const consumer = kafka.consumer({ groupId: groupId })
107
+ const consumer: KafkaConsumer = kafka.consumer({ groupId: groupId })
108
108
  consumers.push(consumer)
109
109
 
110
110
  await consumer.connect()
111
111
  await consumer.subscribe({ topic: topic, fromBeginning: true })
112
112
 
113
- const fn = callback!
114
-
115
113
  await consumer.run({
116
114
  autoCommit: false,
117
115
  eachMessage: async ({ topic, partition, message }) => {
118
- // fn(topic, message.value.toString(), partition) // toString() returns array so won't parse if json.
119
- let success
116
+ // callback(topic, message.value.toString(), partition) // toString() returns array so won't parse if json.
117
+
120
118
  try {
121
- success = await fn(topic, JSON.parse(message.value as string), partition)
119
+ await callback(topic, JSON.parse(message.value as string), partition)
122
120
  } catch (ex) {
123
- success = await fn(topic, message.value, partition)
121
+ await callback(topic, message.value as T, partition) // NOTE: Beware: Ensure Kafka.receive<T> can handle raw message type that is not json parseable
124
122
  }
125
123
 
126
124
  await consumer.commitOffsets([{ topic, partition, offset: (Number(message.offset) + 1).toString() }]);
@@ -160,9 +158,9 @@ const start_producer = async function (topic: string, message: object, key: stri
160
158
  // start_consumer('quickstart-events')
161
159
  // start_producer('quickstart-events', 'Hello KafkaJS user! Little')
162
160
 
163
- export const receive = async function(topic: string, groupId: string, callback?: KafkaCallback) {
161
+ export const receive = async function<T>(topic: string, groupId: string, callback: KafkaCallback<T>): Promise<KafkaConsumer> {
164
162
  topic = topicPrefix + topic
165
- return (await start_consumer(topic, groupId, callback))
163
+ return await start_consumer(topic, groupId, callback)
166
164
  }
167
165
 
168
166
 
package/libs/messaging.ts CHANGED
@@ -59,7 +59,7 @@ type UserPool = {
59
59
  listeners: UserListeners
60
60
  }
61
61
 
62
- type ExpressApp = {
62
+ export type ExpressApp = {
63
63
  get: (
64
64
  path: string,
65
65
  handler: (req: Request, res: Response) => Promise<void>
@@ -102,7 +102,7 @@ async function connect({user, req, res, uid}: {user: string, req: Request, res:
102
102
  const listener = (message: string, channel: string) => {
103
103
  if (users_pool[user]) {
104
104
  try {message = JSON.parse(message)} catch {}
105
- for (let {req, res} of users_pool[user].reqres) {
105
+ for (let {res} of users_pool[user].reqres) {
106
106
  res.write(JSON.stringify({message, channel}) + "\n")
107
107
  if (res.flush) // If compression enabled
108
108
  res.flush()
package/libs/utils.ts CHANGED
@@ -40,7 +40,7 @@ export function isEmpty(str: undefined | number | string): boolean { // returns
40
40
  export let GLOBAL_META = { company: "GLOBAL", outlet: "GLOBAL" }
41
41
 
42
42
  export function isEmptyJson(json: object): boolean {
43
- for (let i in json)
43
+ for (let _i in json)
44
44
  return false
45
45
  return true
46
46
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "corebasic",
3
3
  "type": "module",
4
- "version": "1.0.231",
4
+ "version": "1.0.233",
5
5
  "description": "",
6
6
  "main": "dist/index.js",
7
7
  "types": "./index.ts",