opticedge-cloud-utils 1.1.17 → 1.1.19

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/dist/index.d.ts CHANGED
@@ -7,6 +7,7 @@ export * from './auth';
7
7
  export * from './chunk';
8
8
  export * from './env';
9
9
  export * from './parser';
10
+ export * from './pub';
10
11
  export * from './regex';
11
12
  export * from './retry';
12
13
  export * from './secrets';
package/dist/index.js CHANGED
@@ -23,6 +23,7 @@ __exportStar(require("./auth"), exports);
23
23
  __exportStar(require("./chunk"), exports);
24
24
  __exportStar(require("./env"), exports);
25
25
  __exportStar(require("./parser"), exports);
26
+ __exportStar(require("./pub"), exports);
26
27
  __exportStar(require("./regex"), exports);
27
28
  __exportStar(require("./retry"), exports);
28
29
  __exportStar(require("./secrets"), exports);
package/dist/pub.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function publishMessage(projectId: string, topicName: string, envelope: Record<string, unknown>): Promise<string>;
package/dist/pub.js ADDED
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.publishMessage = publishMessage;
4
+ // src/pub.ts
5
+ const pubsub_1 = require("@google-cloud/pubsub");
6
+ let cachedPubSub = null;
7
+ function getPubSub(projectId) {
8
+ if (!projectId)
9
+ throw new Error('projectId is required');
10
+ if (!cachedPubSub) {
11
+ cachedPubSub = new pubsub_1.PubSub({ projectId });
12
+ }
13
+ return cachedPubSub;
14
+ }
15
+ async function publishMessage(projectId, topicName, envelope) {
16
+ if (!projectId)
17
+ throw new Error('projectId is required');
18
+ if (!topicName)
19
+ throw new Error('topicName is required');
20
+ if (envelope === undefined)
21
+ throw new Error('envelope is required');
22
+ try {
23
+ const pubsub = getPubSub(projectId);
24
+ const topic = pubsub.topic(topicName);
25
+ const data = Buffer.from(JSON.stringify(envelope));
26
+ const messageId = await topic.publishMessage({
27
+ data
28
+ });
29
+ console.info(`INFO: Pub/Sub publish topic=${topicName} msg_id=${messageId}`);
30
+ return messageId;
31
+ }
32
+ catch (err) {
33
+ console.error('ERROR: publish failed:', err);
34
+ throw err;
35
+ }
36
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opticedge-cloud-utils",
3
- "version": "1.1.17",
3
+ "version": "1.1.19",
4
4
  "description": "Common utilities for cloud functions",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -16,6 +16,7 @@
16
16
  "author": "Evans Musonda",
17
17
  "license": "MIT",
18
18
  "dependencies": {
19
+ "@google-cloud/pubsub": "^5.2.0",
19
20
  "@google-cloud/secret-manager": "^6.0.1",
20
21
  "@google-cloud/tasks": "^6.1.0",
21
22
  "axios": "^1.10.0",
package/src/index.ts CHANGED
@@ -7,6 +7,7 @@ export * from './auth'
7
7
  export * from './chunk'
8
8
  export * from './env'
9
9
  export * from './parser'
10
+ export * from './pub'
10
11
  export * from './regex'
11
12
  export * from './retry'
12
13
  export * from './secrets'
package/src/pub.ts ADDED
@@ -0,0 +1,39 @@
1
+ // src/pub.ts
2
+ import { PubSub } from '@google-cloud/pubsub'
3
+
4
+ let cachedPubSub: PubSub | null = null
5
+
6
+ function getPubSub(projectId: string): PubSub {
7
+ if (!projectId) throw new Error('projectId is required')
8
+
9
+ if (!cachedPubSub) {
10
+ cachedPubSub = new PubSub({ projectId })
11
+ }
12
+
13
+ return cachedPubSub
14
+ }
15
+
16
+ export async function publishMessage(
17
+ projectId: string,
18
+ topicName: string,
19
+ envelope: Record<string, unknown>
20
+ ): Promise<string> {
21
+ if (!projectId) throw new Error('projectId is required')
22
+ if (!topicName) throw new Error('topicName is required')
23
+ if (envelope === undefined) throw new Error('envelope is required')
24
+
25
+ try {
26
+ const pubsub = getPubSub(projectId)
27
+ const topic = pubsub.topic(topicName)
28
+ const data = Buffer.from(JSON.stringify(envelope))
29
+ const messageId = await topic.publishMessage({
30
+ data
31
+ })
32
+
33
+ console.info(`INFO: Pub/Sub publish topic=${topicName} msg_id=${messageId}`)
34
+ return messageId
35
+ } catch (err) {
36
+ console.error('ERROR: publish failed:', err)
37
+ throw err
38
+ }
39
+ }
@@ -0,0 +1,127 @@
1
+ // tests/pub.test.ts
2
+ jest.mock('@google-cloud/pubsub', () => {
3
+ const instances: any[] = []
4
+
5
+ class PubSub {
6
+ opts: any
7
+ topics = new Map<string, any>()
8
+ constructor(opts?: any) {
9
+ this.opts = opts
10
+ instances.push(this)
11
+ }
12
+ topic(name: string) {
13
+ if (!this.topics.has(name)) {
14
+ const publishMessage = jest.fn(({ data }: { data: Buffer }) =>
15
+ Promise.resolve(`${this.opts?.projectId}-${name}`)
16
+ )
17
+ this.topics.set(name, { publishMessage })
18
+ }
19
+ return this.topics.get(name)
20
+ }
21
+ }
22
+
23
+ return { PubSub, __instances: instances }
24
+ })
25
+
26
+ const path = '../src/pub'
27
+
28
+ describe('publishMessage', () => {
29
+ beforeEach(() => {
30
+ // ensure fresh module state between tests
31
+ jest.resetModules()
32
+ // clear any existing mock instances array if present
33
+ try {
34
+ const mockPubsub = require('@google-cloud/pubsub')
35
+ if (mockPubsub && Array.isArray(mockPubsub.__instances)) {
36
+ mockPubsub.__instances.length = 0
37
+ }
38
+ } catch {
39
+ // ignore if not loaded yet
40
+ }
41
+ })
42
+
43
+ test('publishes and returns messageId, sends JSON buffer', async () => {
44
+ const mockPubsub = require('@google-cloud/pubsub')
45
+ const { publishMessage } = require(path)
46
+ const envelope = { hello: 'world' }
47
+
48
+ const msgId = await publishMessage('project-1', 'topic-a', envelope)
49
+ expect(msgId).toBe('project-1-topic-a')
50
+
51
+ const instance = mockPubsub.__instances[0]
52
+ expect(instance).toBeDefined()
53
+
54
+ const publishMock = instance.topics.get('topic-a').publishMessage
55
+ expect(publishMock).toHaveBeenCalledTimes(1)
56
+
57
+ const callArg = publishMock.mock.calls[0][0]
58
+ expect(callArg).toHaveProperty('data')
59
+ expect(callArg.data.toString()).toBe(JSON.stringify(envelope))
60
+ })
61
+
62
+ test('throws on missing arguments', async () => {
63
+ const { publishMessage } = require(path)
64
+ await expect(publishMessage('', 't', {})).rejects.toThrow('projectId is required')
65
+ await expect(publishMessage('p', '', {})).rejects.toThrow('topicName is required')
66
+ await expect(publishMessage('p', 't', undefined as any)).rejects.toThrow('envelope is required')
67
+ })
68
+
69
+ test('caches one PubSub instance across multiple calls for the same project', async () => {
70
+ const mockPubsub = require('@google-cloud/pubsub')
71
+ const { publishMessage } = require(path)
72
+
73
+ await publishMessage('project-1', 'topic-x', { a: 1 })
74
+ await publishMessage('project-1', 'topic-x', { b: 2 })
75
+
76
+ const instances = mockPubsub.__instances
77
+ expect(instances.length).toBe(1)
78
+ const publishMock = instances[0].topics.get('topic-x').publishMessage
79
+ expect(publishMock).toHaveBeenCalledTimes(2)
80
+ })
81
+
82
+ test('reuses the same PubSub instance even when called with a different projectId', async () => {
83
+ const mockPubsub = require('@google-cloud/pubsub')
84
+ const { publishMessage } = require(path)
85
+
86
+ // first call creates instance with project-A
87
+ await publishMessage('project-A', 't1', { x: 1 })
88
+ // second call passes a different project id but module currently reuses the cached instance
89
+ await publishMessage('project-B', 't2', { y: 2 })
90
+
91
+ const instances = mockPubsub.__instances
92
+ // because src/pub.ts caches a single PubSub instance, only one instance should exist
93
+ expect(instances.length).toBe(1)
94
+ // the created instance was constructed with the first project id
95
+ expect(instances[0].opts).toEqual({ projectId: 'project-A' })
96
+
97
+ // ensure both publishes were invoked on the same instance (different topics)
98
+ expect(instances[0].topics.get('t1').publishMessage).toHaveBeenCalledTimes(1)
99
+ expect(instances[0].topics.get('t2').publishMessage).toHaveBeenCalledTimes(1)
100
+ })
101
+
102
+ test('logs error and rethrows when publish fails', async () => {
103
+ const mockPubsub = require('@google-cloud/pubsub')
104
+
105
+ // Override the mock PubSub.topic to return a publishMessage that rejects
106
+ mockPubsub.PubSub.prototype.topic = function (name: string) {
107
+ if (!this.topics.has(name)) {
108
+ const publishMessage = jest.fn(() => Promise.reject(new Error('boom')))
109
+ this.topics.set(name, { publishMessage })
110
+ }
111
+ return this.topics.get(name)
112
+ }
113
+
114
+ const { publishMessage } = require(path)
115
+
116
+ const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {})
117
+
118
+ await expect(publishMessage('project-err', 'topic-err', { fail: true })).rejects.toThrow('boom')
119
+
120
+ expect(consoleErrorSpy).toHaveBeenCalled()
121
+ const firstArg = consoleErrorSpy.mock.calls[0][0]
122
+ expect(typeof firstArg).toBe('string')
123
+ expect(firstArg).toContain('ERROR: publish failed:')
124
+
125
+ consoleErrorSpy.mockRestore()
126
+ })
127
+ })