@pubber-subber/gcp-pubsub 0.0.1
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/LICENSE +21 -0
- package/README.md +109 -0
- package/dist/index.cjs +135 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +31 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +133 -0
- package/dist/index.js.map +1 -0
- package/package.json +67 -0
- package/src/index.ts +188 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sami Mishal
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# @pubber-subber/gcp-pubsub
|
|
2
|
+
|
|
3
|
+
[Google Cloud Pub/Sub](https://cloud.google.com/pubsub) adapter for [`@pubber-subber/core`](https://www.npmjs.com/package/@pubber-subber/core). Backed by the official [`@google-cloud/pubsub`](https://www.npmjs.com/package/@google-cloud/pubsub) Node SDK.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
pnpm add @pubber-subber/core @pubber-subber/gcp-pubsub @google-cloud/pubsub
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
`@google-cloud/pubsub` is a **peer dependency**. Auth follows GCP defaults — `GOOGLE_APPLICATION_CREDENTIALS`, Workload Identity, etc.
|
|
12
|
+
|
|
13
|
+
## Quick start
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { PubSub } from '@pubber-subber/core';
|
|
17
|
+
import { gcpPubSub } from '@pubber-subber/gcp-pubsub';
|
|
18
|
+
|
|
19
|
+
const pubsub = new PubSub({
|
|
20
|
+
adapter: gcpPubSub({ projectId: 'my-project' }),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
await pubsub.subscribe(
|
|
24
|
+
'users.created',
|
|
25
|
+
(msg) => console.log(msg.payload, msg.meta?.attributes),
|
|
26
|
+
{
|
|
27
|
+
subscriptionName: 'workers-users-created',
|
|
28
|
+
createIfMissing: true,
|
|
29
|
+
},
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
await pubsub.publish(
|
|
33
|
+
'users.created',
|
|
34
|
+
{ id: 1 },
|
|
35
|
+
{ attributes: { tenantId: 't1' }, orderingKey: 'user-1' },
|
|
36
|
+
);
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Options
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
gcpPubSub({
|
|
43
|
+
projectId?: string;
|
|
44
|
+
client?: GooglePubSub; // pre-built @google-cloud/pubsub client
|
|
45
|
+
options?: ClientConfig;
|
|
46
|
+
codec?: Codec;
|
|
47
|
+
subscriptionPrefix?: string; // default: 'pubber'
|
|
48
|
+
})
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
| Option | Notes |
|
|
52
|
+
| --- | --- |
|
|
53
|
+
| `projectId` | GCP project. Falls back to `options.projectId` / SDK auto-detection. |
|
|
54
|
+
| `client` | Pre-constructed `PubSub` client from `@google-cloud/pubsub`. Useful when you want to share a client. |
|
|
55
|
+
| `options` | Full `ClientConfig` from the SDK (transport, scopes, etc.). |
|
|
56
|
+
| `codec` | Payload encoder/decoder. Default `jsonCodec()`. |
|
|
57
|
+
| `subscriptionPrefix` | Prefix for auto-generated subscription names when `subscriptionName` isn't provided. |
|
|
58
|
+
|
|
59
|
+
## Publish meta
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
await pubsub.publish('users.created', payload, {
|
|
63
|
+
attributes: { tenantId: 't1' },
|
|
64
|
+
orderingKey: 'user-1',
|
|
65
|
+
});
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
| Field | Notes |
|
|
69
|
+
| --- | --- |
|
|
70
|
+
| `attributes` | `Record<string, string>`, forwarded to Pub/Sub message attributes. |
|
|
71
|
+
| `orderingKey` | Forwarded as the ordering key. Requires the topic to have message ordering enabled. |
|
|
72
|
+
|
|
73
|
+
## Subscribe meta
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
await pubsub.subscribe('users.created', handler, {
|
|
77
|
+
subscriptionName: 'workers-users-created',
|
|
78
|
+
createIfMissing: true,
|
|
79
|
+
});
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
| Field | Notes |
|
|
83
|
+
| --- | --- |
|
|
84
|
+
| `subscriptionName` | Existing subscription. **Required for production** — every worker must share the same subscription name to load-balance message processing. If omitted, a unique per-process name is generated (suitable for dev / one-off scripts only). |
|
|
85
|
+
| `createIfMissing` | Create the subscription on demand if it doesn't exist. Useful for ephemeral or dev subscriptions. |
|
|
86
|
+
| `subscriptionOptions` | Forwarded to `topic.createSubscription()` (e.g. `ackDeadlineSeconds`, `messageRetentionDuration`). |
|
|
87
|
+
| `ephemeral` | Delete the subscription on `unsubscribe`. Use in tests; **never** in production. |
|
|
88
|
+
|
|
89
|
+
## Capabilities
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
{ publish: true, subscribe: true, patternSubscribe: false, ack: true }
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Ack semantics
|
|
96
|
+
|
|
97
|
+
- Handler returns successfully → `message.ack()`.
|
|
98
|
+
- Handler throws → `message.nack()`. The server redelivers per the subscription's retry policy.
|
|
99
|
+
- For manual control, call `msg.ack()` or `msg.nack()` inside your handler. The adapter tracks whether either was called and skips the auto-resolve to avoid double-ack.
|
|
100
|
+
|
|
101
|
+
## Notes
|
|
102
|
+
|
|
103
|
+
- Topic names use Pub/Sub's standard format. The adapter passes them through as-is — you can use bare names like `users.created` (resolved against the configured project) or fully qualified `projects/my-project/topics/users.created`.
|
|
104
|
+
- For local development, the [Pub/Sub emulator](https://cloud.google.com/pubsub/docs/emulator) works out of the box: set `PUBSUB_EMULATOR_HOST=localhost:8085` and the SDK routes to it automatically.
|
|
105
|
+
- The adapter doesn't auto-create topics — only subscriptions (via `createIfMissing`). Topics should exist before you publish.
|
|
106
|
+
|
|
107
|
+
## License
|
|
108
|
+
|
|
109
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var pubsub = require('@google-cloud/pubsub');
|
|
4
|
+
var core = require('@pubber-subber/core');
|
|
5
|
+
|
|
6
|
+
// src/index.ts
|
|
7
|
+
function gcpPubSub(opts = {}) {
|
|
8
|
+
const codec = opts.codec ?? core.jsonCodec();
|
|
9
|
+
let client = null;
|
|
10
|
+
let idCounter = 0;
|
|
11
|
+
const liveSubscriptions = /* @__PURE__ */ new Set();
|
|
12
|
+
const ensure = () => {
|
|
13
|
+
if (client) return client;
|
|
14
|
+
if (opts.client) {
|
|
15
|
+
client = opts.client;
|
|
16
|
+
} else {
|
|
17
|
+
client = new pubsub.PubSub({ projectId: opts.projectId, ...opts.options });
|
|
18
|
+
}
|
|
19
|
+
return client;
|
|
20
|
+
};
|
|
21
|
+
return {
|
|
22
|
+
name: "gcp-pubsub",
|
|
23
|
+
capabilities: { publish: true, subscribe: true, patternSubscribe: false, ack: true },
|
|
24
|
+
async connect() {
|
|
25
|
+
ensure();
|
|
26
|
+
},
|
|
27
|
+
async disconnect() {
|
|
28
|
+
for (const s of [...liveSubscriptions]) {
|
|
29
|
+
await s.close().catch(noop);
|
|
30
|
+
}
|
|
31
|
+
liveSubscriptions.clear();
|
|
32
|
+
if (client) {
|
|
33
|
+
await client.close().catch(noop);
|
|
34
|
+
client = null;
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
async publish(topic, payload, meta) {
|
|
38
|
+
const c = ensure();
|
|
39
|
+
const t = c.topic(topic);
|
|
40
|
+
const encoded = codec.encode(payload);
|
|
41
|
+
const data = typeof encoded === "string" ? Buffer.from(encoded, "utf8") : Buffer.from(encoded);
|
|
42
|
+
await t.publishMessage({
|
|
43
|
+
data,
|
|
44
|
+
attributes: meta?.attributes,
|
|
45
|
+
orderingKey: meta?.orderingKey
|
|
46
|
+
});
|
|
47
|
+
},
|
|
48
|
+
async subscribe(topic, handler, meta) {
|
|
49
|
+
const c = ensure();
|
|
50
|
+
const prefix = opts.subscriptionPrefix ?? "pubber";
|
|
51
|
+
const subName = meta?.subscriptionName ?? `${prefix}-${sanitize(topic)}-${process.pid}-${Date.now()}-${++idCounter}`;
|
|
52
|
+
let subscription = c.subscription(subName);
|
|
53
|
+
if (meta?.createIfMissing) {
|
|
54
|
+
const [exists] = await subscription.exists();
|
|
55
|
+
if (!exists) {
|
|
56
|
+
try {
|
|
57
|
+
await c.topic(topic).createSubscription(subName, meta.subscriptionOptions);
|
|
58
|
+
} catch (err) {
|
|
59
|
+
throw new core.SubscriptionError(
|
|
60
|
+
`Failed to create subscription "${subName}" on topic "${topic}"`,
|
|
61
|
+
{ cause: err }
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
subscription = c.subscription(subName);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const onMessage = async (message) => {
|
|
68
|
+
let payload = message.data;
|
|
69
|
+
try {
|
|
70
|
+
payload = codec.decode(message.data);
|
|
71
|
+
} catch {
|
|
72
|
+
}
|
|
73
|
+
let resolved = false;
|
|
74
|
+
const adapterMsg = {
|
|
75
|
+
topic,
|
|
76
|
+
payload,
|
|
77
|
+
raw: message,
|
|
78
|
+
meta: {
|
|
79
|
+
attributes: message.attributes,
|
|
80
|
+
messageId: message.id,
|
|
81
|
+
publishTime: message.publishTime,
|
|
82
|
+
orderingKey: message.orderingKey
|
|
83
|
+
},
|
|
84
|
+
ack: async () => {
|
|
85
|
+
if (resolved) return;
|
|
86
|
+
resolved = true;
|
|
87
|
+
message.ack();
|
|
88
|
+
},
|
|
89
|
+
nack: async () => {
|
|
90
|
+
if (resolved) return;
|
|
91
|
+
resolved = true;
|
|
92
|
+
message.nack();
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
try {
|
|
96
|
+
await handler(adapterMsg);
|
|
97
|
+
if (!resolved) {
|
|
98
|
+
resolved = true;
|
|
99
|
+
message.ack();
|
|
100
|
+
}
|
|
101
|
+
} catch {
|
|
102
|
+
if (!resolved) {
|
|
103
|
+
resolved = true;
|
|
104
|
+
message.nack();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
subscription.on("message", onMessage);
|
|
109
|
+
liveSubscriptions.add(subscription);
|
|
110
|
+
idCounter += 1;
|
|
111
|
+
const id = `gcp-${idCounter}`;
|
|
112
|
+
return {
|
|
113
|
+
id,
|
|
114
|
+
topic,
|
|
115
|
+
unsubscribe: async () => {
|
|
116
|
+
subscription.off("message", onMessage);
|
|
117
|
+
await subscription.close().catch(noop);
|
|
118
|
+
liveSubscriptions.delete(subscription);
|
|
119
|
+
if (meta?.ephemeral) {
|
|
120
|
+
await subscription.delete().catch(noop);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
function sanitize(name) {
|
|
128
|
+
return name.replace(/[^a-zA-Z0-9_\-.~+%]/g, "-").slice(0, 80);
|
|
129
|
+
}
|
|
130
|
+
function noop() {
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
exports.gcpPubSub = gcpPubSub;
|
|
134
|
+
//# sourceMappingURL=index.cjs.map
|
|
135
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":["jsonCodec","GooglePubSub","SubscriptionError"],"mappings":"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","file":"index.cjs","sourcesContent":["import {\n type ClientConfig,\n type CreateSubscriptionOptions,\n PubSub as GooglePubSub,\n type Message,\n type Subscription,\n} from '@google-cloud/pubsub';\nimport {\n type AdapterMessage,\n type Codec,\n type PubSubAdapter,\n SubscriptionError,\n jsonCodec,\n} from '@pubber-subber/core';\n\nexport interface GcpPubSubAdapterOptions {\n projectId?: string;\n /** Or pass a fully-constructed client. */\n client?: GooglePubSub;\n /** Or `ClientConfig` for the underlying SDK. */\n options?: ClientConfig;\n /** Payload codec. Default: JSON. */\n codec?: Codec;\n /** Prefix for auto-generated subscription names. Default: `pubber`. */\n subscriptionPrefix?: string;\n}\n\nexport interface GcpPublishMeta {\n attributes?: Record<string, string>;\n orderingKey?: string;\n}\n\nexport interface GcpSubscribeMeta {\n /** Existing subscription name. If omitted, a unique name is generated. */\n subscriptionName?: string;\n /** Create the subscription if it doesn't exist. */\n createIfMissing?: boolean;\n /** Subscription options used when `createIfMissing` is true. */\n subscriptionOptions?: CreateSubscriptionOptions;\n /** Delete the subscription when the handle is unsubscribed. Best for dev. */\n ephemeral?: boolean;\n}\n\nexport function gcpPubSub(\n opts: GcpPubSubAdapterOptions = {},\n): PubSubAdapter<GcpPublishMeta, GcpSubscribeMeta> {\n const codec = opts.codec ?? jsonCodec();\n let client: GooglePubSub | null = null;\n let idCounter = 0;\n const liveSubscriptions = new Set<Subscription>();\n\n const ensure = (): GooglePubSub => {\n if (client) return client;\n if (opts.client) {\n client = opts.client;\n } else {\n client = new GooglePubSub({ projectId: opts.projectId, ...opts.options });\n }\n return client;\n };\n\n return {\n name: 'gcp-pubsub',\n capabilities: { publish: true, subscribe: true, patternSubscribe: false, ack: true },\n\n async connect() {\n ensure();\n },\n\n async disconnect() {\n for (const s of [...liveSubscriptions]) {\n await s.close().catch(noop);\n }\n liveSubscriptions.clear();\n if (client) {\n await client.close().catch(noop);\n client = null;\n }\n },\n\n async publish(topic, payload, meta) {\n const c = ensure();\n const t = c.topic(topic);\n const encoded = codec.encode(payload);\n const data =\n typeof encoded === 'string' ? Buffer.from(encoded, 'utf8') : Buffer.from(encoded);\n await t.publishMessage({\n data,\n attributes: meta?.attributes,\n orderingKey: meta?.orderingKey,\n });\n },\n\n async subscribe(topic, handler, meta) {\n const c = ensure();\n const prefix = opts.subscriptionPrefix ?? 'pubber';\n const subName =\n meta?.subscriptionName ??\n `${prefix}-${sanitize(topic)}-${process.pid}-${Date.now()}-${++idCounter}`;\n let subscription = c.subscription(subName);\n\n if (meta?.createIfMissing) {\n const [exists] = await subscription.exists();\n if (!exists) {\n try {\n await c.topic(topic).createSubscription(subName, meta.subscriptionOptions);\n } catch (err) {\n throw new SubscriptionError(\n `Failed to create subscription \"${subName}\" on topic \"${topic}\"`,\n { cause: err },\n );\n }\n subscription = c.subscription(subName);\n }\n }\n\n const onMessage = async (message: Message) => {\n let payload: unknown = message.data;\n try {\n payload = codec.decode(message.data);\n } catch {\n // Leave as raw buffer.\n }\n let resolved = false;\n const adapterMsg: AdapterMessage = {\n topic,\n payload,\n raw: message,\n meta: {\n attributes: message.attributes,\n messageId: message.id,\n publishTime: message.publishTime,\n orderingKey: message.orderingKey,\n },\n ack: async () => {\n if (resolved) return;\n resolved = true;\n message.ack();\n },\n nack: async () => {\n if (resolved) return;\n resolved = true;\n message.nack();\n },\n };\n try {\n await handler(adapterMsg);\n if (!resolved) {\n resolved = true;\n message.ack();\n }\n } catch {\n if (!resolved) {\n resolved = true;\n message.nack();\n }\n }\n };\n\n subscription.on('message', onMessage);\n liveSubscriptions.add(subscription);\n\n idCounter += 1;\n const id = `gcp-${idCounter}`;\n return {\n id,\n topic,\n unsubscribe: async () => {\n subscription.off('message', onMessage);\n await subscription.close().catch(noop);\n liveSubscriptions.delete(subscription);\n if (meta?.ephemeral) {\n await subscription.delete().catch(noop);\n }\n },\n };\n },\n };\n}\n\nfunction sanitize(name: string): string {\n // GCP subscription names must match [a-zA-Z][a-zA-Z0-9_\\-.~+%]{2,254}\n return name.replace(/[^a-zA-Z0-9_\\-.~+%]/g, '-').slice(0, 80);\n}\n\nfunction noop(): void {\n // intentional\n}\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { PubSub, ClientConfig, CreateSubscriptionOptions } from '@google-cloud/pubsub';
|
|
2
|
+
import { Codec, PubSubAdapter } from '@pubber-subber/core';
|
|
3
|
+
|
|
4
|
+
interface GcpPubSubAdapterOptions {
|
|
5
|
+
projectId?: string;
|
|
6
|
+
/** Or pass a fully-constructed client. */
|
|
7
|
+
client?: PubSub;
|
|
8
|
+
/** Or `ClientConfig` for the underlying SDK. */
|
|
9
|
+
options?: ClientConfig;
|
|
10
|
+
/** Payload codec. Default: JSON. */
|
|
11
|
+
codec?: Codec;
|
|
12
|
+
/** Prefix for auto-generated subscription names. Default: `pubber`. */
|
|
13
|
+
subscriptionPrefix?: string;
|
|
14
|
+
}
|
|
15
|
+
interface GcpPublishMeta {
|
|
16
|
+
attributes?: Record<string, string>;
|
|
17
|
+
orderingKey?: string;
|
|
18
|
+
}
|
|
19
|
+
interface GcpSubscribeMeta {
|
|
20
|
+
/** Existing subscription name. If omitted, a unique name is generated. */
|
|
21
|
+
subscriptionName?: string;
|
|
22
|
+
/** Create the subscription if it doesn't exist. */
|
|
23
|
+
createIfMissing?: boolean;
|
|
24
|
+
/** Subscription options used when `createIfMissing` is true. */
|
|
25
|
+
subscriptionOptions?: CreateSubscriptionOptions;
|
|
26
|
+
/** Delete the subscription when the handle is unsubscribed. Best for dev. */
|
|
27
|
+
ephemeral?: boolean;
|
|
28
|
+
}
|
|
29
|
+
declare function gcpPubSub(opts?: GcpPubSubAdapterOptions): PubSubAdapter<GcpPublishMeta, GcpSubscribeMeta>;
|
|
30
|
+
|
|
31
|
+
export { type GcpPubSubAdapterOptions, type GcpPublishMeta, type GcpSubscribeMeta, gcpPubSub };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { PubSub, ClientConfig, CreateSubscriptionOptions } from '@google-cloud/pubsub';
|
|
2
|
+
import { Codec, PubSubAdapter } from '@pubber-subber/core';
|
|
3
|
+
|
|
4
|
+
interface GcpPubSubAdapterOptions {
|
|
5
|
+
projectId?: string;
|
|
6
|
+
/** Or pass a fully-constructed client. */
|
|
7
|
+
client?: PubSub;
|
|
8
|
+
/** Or `ClientConfig` for the underlying SDK. */
|
|
9
|
+
options?: ClientConfig;
|
|
10
|
+
/** Payload codec. Default: JSON. */
|
|
11
|
+
codec?: Codec;
|
|
12
|
+
/** Prefix for auto-generated subscription names. Default: `pubber`. */
|
|
13
|
+
subscriptionPrefix?: string;
|
|
14
|
+
}
|
|
15
|
+
interface GcpPublishMeta {
|
|
16
|
+
attributes?: Record<string, string>;
|
|
17
|
+
orderingKey?: string;
|
|
18
|
+
}
|
|
19
|
+
interface GcpSubscribeMeta {
|
|
20
|
+
/** Existing subscription name. If omitted, a unique name is generated. */
|
|
21
|
+
subscriptionName?: string;
|
|
22
|
+
/** Create the subscription if it doesn't exist. */
|
|
23
|
+
createIfMissing?: boolean;
|
|
24
|
+
/** Subscription options used when `createIfMissing` is true. */
|
|
25
|
+
subscriptionOptions?: CreateSubscriptionOptions;
|
|
26
|
+
/** Delete the subscription when the handle is unsubscribed. Best for dev. */
|
|
27
|
+
ephemeral?: boolean;
|
|
28
|
+
}
|
|
29
|
+
declare function gcpPubSub(opts?: GcpPubSubAdapterOptions): PubSubAdapter<GcpPublishMeta, GcpSubscribeMeta>;
|
|
30
|
+
|
|
31
|
+
export { type GcpPubSubAdapterOptions, type GcpPublishMeta, type GcpSubscribeMeta, gcpPubSub };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { PubSub } from '@google-cloud/pubsub';
|
|
2
|
+
import { jsonCodec, SubscriptionError } from '@pubber-subber/core';
|
|
3
|
+
|
|
4
|
+
// src/index.ts
|
|
5
|
+
function gcpPubSub(opts = {}) {
|
|
6
|
+
const codec = opts.codec ?? jsonCodec();
|
|
7
|
+
let client = null;
|
|
8
|
+
let idCounter = 0;
|
|
9
|
+
const liveSubscriptions = /* @__PURE__ */ new Set();
|
|
10
|
+
const ensure = () => {
|
|
11
|
+
if (client) return client;
|
|
12
|
+
if (opts.client) {
|
|
13
|
+
client = opts.client;
|
|
14
|
+
} else {
|
|
15
|
+
client = new PubSub({ projectId: opts.projectId, ...opts.options });
|
|
16
|
+
}
|
|
17
|
+
return client;
|
|
18
|
+
};
|
|
19
|
+
return {
|
|
20
|
+
name: "gcp-pubsub",
|
|
21
|
+
capabilities: { publish: true, subscribe: true, patternSubscribe: false, ack: true },
|
|
22
|
+
async connect() {
|
|
23
|
+
ensure();
|
|
24
|
+
},
|
|
25
|
+
async disconnect() {
|
|
26
|
+
for (const s of [...liveSubscriptions]) {
|
|
27
|
+
await s.close().catch(noop);
|
|
28
|
+
}
|
|
29
|
+
liveSubscriptions.clear();
|
|
30
|
+
if (client) {
|
|
31
|
+
await client.close().catch(noop);
|
|
32
|
+
client = null;
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
async publish(topic, payload, meta) {
|
|
36
|
+
const c = ensure();
|
|
37
|
+
const t = c.topic(topic);
|
|
38
|
+
const encoded = codec.encode(payload);
|
|
39
|
+
const data = typeof encoded === "string" ? Buffer.from(encoded, "utf8") : Buffer.from(encoded);
|
|
40
|
+
await t.publishMessage({
|
|
41
|
+
data,
|
|
42
|
+
attributes: meta?.attributes,
|
|
43
|
+
orderingKey: meta?.orderingKey
|
|
44
|
+
});
|
|
45
|
+
},
|
|
46
|
+
async subscribe(topic, handler, meta) {
|
|
47
|
+
const c = ensure();
|
|
48
|
+
const prefix = opts.subscriptionPrefix ?? "pubber";
|
|
49
|
+
const subName = meta?.subscriptionName ?? `${prefix}-${sanitize(topic)}-${process.pid}-${Date.now()}-${++idCounter}`;
|
|
50
|
+
let subscription = c.subscription(subName);
|
|
51
|
+
if (meta?.createIfMissing) {
|
|
52
|
+
const [exists] = await subscription.exists();
|
|
53
|
+
if (!exists) {
|
|
54
|
+
try {
|
|
55
|
+
await c.topic(topic).createSubscription(subName, meta.subscriptionOptions);
|
|
56
|
+
} catch (err) {
|
|
57
|
+
throw new SubscriptionError(
|
|
58
|
+
`Failed to create subscription "${subName}" on topic "${topic}"`,
|
|
59
|
+
{ cause: err }
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
subscription = c.subscription(subName);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const onMessage = async (message) => {
|
|
66
|
+
let payload = message.data;
|
|
67
|
+
try {
|
|
68
|
+
payload = codec.decode(message.data);
|
|
69
|
+
} catch {
|
|
70
|
+
}
|
|
71
|
+
let resolved = false;
|
|
72
|
+
const adapterMsg = {
|
|
73
|
+
topic,
|
|
74
|
+
payload,
|
|
75
|
+
raw: message,
|
|
76
|
+
meta: {
|
|
77
|
+
attributes: message.attributes,
|
|
78
|
+
messageId: message.id,
|
|
79
|
+
publishTime: message.publishTime,
|
|
80
|
+
orderingKey: message.orderingKey
|
|
81
|
+
},
|
|
82
|
+
ack: async () => {
|
|
83
|
+
if (resolved) return;
|
|
84
|
+
resolved = true;
|
|
85
|
+
message.ack();
|
|
86
|
+
},
|
|
87
|
+
nack: async () => {
|
|
88
|
+
if (resolved) return;
|
|
89
|
+
resolved = true;
|
|
90
|
+
message.nack();
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
try {
|
|
94
|
+
await handler(adapterMsg);
|
|
95
|
+
if (!resolved) {
|
|
96
|
+
resolved = true;
|
|
97
|
+
message.ack();
|
|
98
|
+
}
|
|
99
|
+
} catch {
|
|
100
|
+
if (!resolved) {
|
|
101
|
+
resolved = true;
|
|
102
|
+
message.nack();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
subscription.on("message", onMessage);
|
|
107
|
+
liveSubscriptions.add(subscription);
|
|
108
|
+
idCounter += 1;
|
|
109
|
+
const id = `gcp-${idCounter}`;
|
|
110
|
+
return {
|
|
111
|
+
id,
|
|
112
|
+
topic,
|
|
113
|
+
unsubscribe: async () => {
|
|
114
|
+
subscription.off("message", onMessage);
|
|
115
|
+
await subscription.close().catch(noop);
|
|
116
|
+
liveSubscriptions.delete(subscription);
|
|
117
|
+
if (meta?.ephemeral) {
|
|
118
|
+
await subscription.delete().catch(noop);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function sanitize(name) {
|
|
126
|
+
return name.replace(/[^a-zA-Z0-9_\-.~+%]/g, "-").slice(0, 80);
|
|
127
|
+
}
|
|
128
|
+
function noop() {
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export { gcpPubSub };
|
|
132
|
+
//# sourceMappingURL=index.js.map
|
|
133
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":["GooglePubSub"],"mappings":"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","file":"index.js","sourcesContent":["import {\n type ClientConfig,\n type CreateSubscriptionOptions,\n PubSub as GooglePubSub,\n type Message,\n type Subscription,\n} from '@google-cloud/pubsub';\nimport {\n type AdapterMessage,\n type Codec,\n type PubSubAdapter,\n SubscriptionError,\n jsonCodec,\n} from '@pubber-subber/core';\n\nexport interface GcpPubSubAdapterOptions {\n projectId?: string;\n /** Or pass a fully-constructed client. */\n client?: GooglePubSub;\n /** Or `ClientConfig` for the underlying SDK. */\n options?: ClientConfig;\n /** Payload codec. Default: JSON. */\n codec?: Codec;\n /** Prefix for auto-generated subscription names. Default: `pubber`. */\n subscriptionPrefix?: string;\n}\n\nexport interface GcpPublishMeta {\n attributes?: Record<string, string>;\n orderingKey?: string;\n}\n\nexport interface GcpSubscribeMeta {\n /** Existing subscription name. If omitted, a unique name is generated. */\n subscriptionName?: string;\n /** Create the subscription if it doesn't exist. */\n createIfMissing?: boolean;\n /** Subscription options used when `createIfMissing` is true. */\n subscriptionOptions?: CreateSubscriptionOptions;\n /** Delete the subscription when the handle is unsubscribed. Best for dev. */\n ephemeral?: boolean;\n}\n\nexport function gcpPubSub(\n opts: GcpPubSubAdapterOptions = {},\n): PubSubAdapter<GcpPublishMeta, GcpSubscribeMeta> {\n const codec = opts.codec ?? jsonCodec();\n let client: GooglePubSub | null = null;\n let idCounter = 0;\n const liveSubscriptions = new Set<Subscription>();\n\n const ensure = (): GooglePubSub => {\n if (client) return client;\n if (opts.client) {\n client = opts.client;\n } else {\n client = new GooglePubSub({ projectId: opts.projectId, ...opts.options });\n }\n return client;\n };\n\n return {\n name: 'gcp-pubsub',\n capabilities: { publish: true, subscribe: true, patternSubscribe: false, ack: true },\n\n async connect() {\n ensure();\n },\n\n async disconnect() {\n for (const s of [...liveSubscriptions]) {\n await s.close().catch(noop);\n }\n liveSubscriptions.clear();\n if (client) {\n await client.close().catch(noop);\n client = null;\n }\n },\n\n async publish(topic, payload, meta) {\n const c = ensure();\n const t = c.topic(topic);\n const encoded = codec.encode(payload);\n const data =\n typeof encoded === 'string' ? Buffer.from(encoded, 'utf8') : Buffer.from(encoded);\n await t.publishMessage({\n data,\n attributes: meta?.attributes,\n orderingKey: meta?.orderingKey,\n });\n },\n\n async subscribe(topic, handler, meta) {\n const c = ensure();\n const prefix = opts.subscriptionPrefix ?? 'pubber';\n const subName =\n meta?.subscriptionName ??\n `${prefix}-${sanitize(topic)}-${process.pid}-${Date.now()}-${++idCounter}`;\n let subscription = c.subscription(subName);\n\n if (meta?.createIfMissing) {\n const [exists] = await subscription.exists();\n if (!exists) {\n try {\n await c.topic(topic).createSubscription(subName, meta.subscriptionOptions);\n } catch (err) {\n throw new SubscriptionError(\n `Failed to create subscription \"${subName}\" on topic \"${topic}\"`,\n { cause: err },\n );\n }\n subscription = c.subscription(subName);\n }\n }\n\n const onMessage = async (message: Message) => {\n let payload: unknown = message.data;\n try {\n payload = codec.decode(message.data);\n } catch {\n // Leave as raw buffer.\n }\n let resolved = false;\n const adapterMsg: AdapterMessage = {\n topic,\n payload,\n raw: message,\n meta: {\n attributes: message.attributes,\n messageId: message.id,\n publishTime: message.publishTime,\n orderingKey: message.orderingKey,\n },\n ack: async () => {\n if (resolved) return;\n resolved = true;\n message.ack();\n },\n nack: async () => {\n if (resolved) return;\n resolved = true;\n message.nack();\n },\n };\n try {\n await handler(adapterMsg);\n if (!resolved) {\n resolved = true;\n message.ack();\n }\n } catch {\n if (!resolved) {\n resolved = true;\n message.nack();\n }\n }\n };\n\n subscription.on('message', onMessage);\n liveSubscriptions.add(subscription);\n\n idCounter += 1;\n const id = `gcp-${idCounter}`;\n return {\n id,\n topic,\n unsubscribe: async () => {\n subscription.off('message', onMessage);\n await subscription.close().catch(noop);\n liveSubscriptions.delete(subscription);\n if (meta?.ephemeral) {\n await subscription.delete().catch(noop);\n }\n },\n };\n },\n };\n}\n\nfunction sanitize(name: string): string {\n // GCP subscription names must match [a-zA-Z][a-zA-Z0-9_\\-.~+%]{2,254}\n return name.replace(/[^a-zA-Z0-9_\\-.~+%]/g, '-').slice(0, 80);\n}\n\nfunction noop(): void {\n // intentional\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pubber-subber/gcp-pubsub",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Google Cloud Pub/Sub adapter for @pubber-subber.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pubsub",
|
|
7
|
+
"pub-sub",
|
|
8
|
+
"messaging",
|
|
9
|
+
"events",
|
|
10
|
+
"adapter",
|
|
11
|
+
"gcp",
|
|
12
|
+
"google-cloud",
|
|
13
|
+
"google-cloud-pubsub",
|
|
14
|
+
"typescript"
|
|
15
|
+
],
|
|
16
|
+
"homepage": "https://github.com/samishal1998/pubber-subber/tree/main/packages/gcp-pubsub#readme",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/samishal1998/pubber-subber.git",
|
|
20
|
+
"directory": "packages/gcp-pubsub"
|
|
21
|
+
},
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://github.com/samishal1998/pubber-subber/issues"
|
|
24
|
+
},
|
|
25
|
+
"author": "Sami Mishal",
|
|
26
|
+
"type": "module",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"sideEffects": false,
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"import": {
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"default": "./dist/index.js"
|
|
34
|
+
},
|
|
35
|
+
"require": {
|
|
36
|
+
"types": "./dist/index.d.cts",
|
|
37
|
+
"default": "./dist/index.cjs"
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"./package.json": "./package.json"
|
|
41
|
+
},
|
|
42
|
+
"main": "./dist/index.cjs",
|
|
43
|
+
"module": "./dist/index.js",
|
|
44
|
+
"types": "./dist/index.d.ts",
|
|
45
|
+
"files": ["dist", "src", "README.md", "LICENSE"],
|
|
46
|
+
"scripts": {
|
|
47
|
+
"prebuild": "node ../../scripts/swap-package-json.mjs build",
|
|
48
|
+
"build": "tsup",
|
|
49
|
+
"typecheck": "tsc --noEmit",
|
|
50
|
+
"test": "vitest run",
|
|
51
|
+
"test:integration": "vitest run",
|
|
52
|
+
"test:watch": "vitest",
|
|
53
|
+
"prepublishOnly": "node ../../scripts/swap-package-json.mjs publish",
|
|
54
|
+
"postpublish": "node ../../scripts/swap-package-json.mjs build"
|
|
55
|
+
},
|
|
56
|
+
"peerDependencies": {
|
|
57
|
+
"@google-cloud/pubsub": "^4.0.0 || ^5.0.0",
|
|
58
|
+
"@pubber-subber/core": "^0.0.1"
|
|
59
|
+
},
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"@pubber-subber/core": "workspace:*",
|
|
62
|
+
"@google-cloud/pubsub": "^4.9.0"
|
|
63
|
+
},
|
|
64
|
+
"publishConfig": {
|
|
65
|
+
"access": "public"
|
|
66
|
+
}
|
|
67
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ClientConfig,
|
|
3
|
+
type CreateSubscriptionOptions,
|
|
4
|
+
PubSub as GooglePubSub,
|
|
5
|
+
type Message,
|
|
6
|
+
type Subscription,
|
|
7
|
+
} from '@google-cloud/pubsub';
|
|
8
|
+
import {
|
|
9
|
+
type AdapterMessage,
|
|
10
|
+
type Codec,
|
|
11
|
+
type PubSubAdapter,
|
|
12
|
+
SubscriptionError,
|
|
13
|
+
jsonCodec,
|
|
14
|
+
} from '@pubber-subber/core';
|
|
15
|
+
|
|
16
|
+
export interface GcpPubSubAdapterOptions {
|
|
17
|
+
projectId?: string;
|
|
18
|
+
/** Or pass a fully-constructed client. */
|
|
19
|
+
client?: GooglePubSub;
|
|
20
|
+
/** Or `ClientConfig` for the underlying SDK. */
|
|
21
|
+
options?: ClientConfig;
|
|
22
|
+
/** Payload codec. Default: JSON. */
|
|
23
|
+
codec?: Codec;
|
|
24
|
+
/** Prefix for auto-generated subscription names. Default: `pubber`. */
|
|
25
|
+
subscriptionPrefix?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface GcpPublishMeta {
|
|
29
|
+
attributes?: Record<string, string>;
|
|
30
|
+
orderingKey?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface GcpSubscribeMeta {
|
|
34
|
+
/** Existing subscription name. If omitted, a unique name is generated. */
|
|
35
|
+
subscriptionName?: string;
|
|
36
|
+
/** Create the subscription if it doesn't exist. */
|
|
37
|
+
createIfMissing?: boolean;
|
|
38
|
+
/** Subscription options used when `createIfMissing` is true. */
|
|
39
|
+
subscriptionOptions?: CreateSubscriptionOptions;
|
|
40
|
+
/** Delete the subscription when the handle is unsubscribed. Best for dev. */
|
|
41
|
+
ephemeral?: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function gcpPubSub(
|
|
45
|
+
opts: GcpPubSubAdapterOptions = {},
|
|
46
|
+
): PubSubAdapter<GcpPublishMeta, GcpSubscribeMeta> {
|
|
47
|
+
const codec = opts.codec ?? jsonCodec();
|
|
48
|
+
let client: GooglePubSub | null = null;
|
|
49
|
+
let idCounter = 0;
|
|
50
|
+
const liveSubscriptions = new Set<Subscription>();
|
|
51
|
+
|
|
52
|
+
const ensure = (): GooglePubSub => {
|
|
53
|
+
if (client) return client;
|
|
54
|
+
if (opts.client) {
|
|
55
|
+
client = opts.client;
|
|
56
|
+
} else {
|
|
57
|
+
client = new GooglePubSub({ projectId: opts.projectId, ...opts.options });
|
|
58
|
+
}
|
|
59
|
+
return client;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
name: 'gcp-pubsub',
|
|
64
|
+
capabilities: { publish: true, subscribe: true, patternSubscribe: false, ack: true },
|
|
65
|
+
|
|
66
|
+
async connect() {
|
|
67
|
+
ensure();
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
async disconnect() {
|
|
71
|
+
for (const s of [...liveSubscriptions]) {
|
|
72
|
+
await s.close().catch(noop);
|
|
73
|
+
}
|
|
74
|
+
liveSubscriptions.clear();
|
|
75
|
+
if (client) {
|
|
76
|
+
await client.close().catch(noop);
|
|
77
|
+
client = null;
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
async publish(topic, payload, meta) {
|
|
82
|
+
const c = ensure();
|
|
83
|
+
const t = c.topic(topic);
|
|
84
|
+
const encoded = codec.encode(payload);
|
|
85
|
+
const data =
|
|
86
|
+
typeof encoded === 'string' ? Buffer.from(encoded, 'utf8') : Buffer.from(encoded);
|
|
87
|
+
await t.publishMessage({
|
|
88
|
+
data,
|
|
89
|
+
attributes: meta?.attributes,
|
|
90
|
+
orderingKey: meta?.orderingKey,
|
|
91
|
+
});
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
async subscribe(topic, handler, meta) {
|
|
95
|
+
const c = ensure();
|
|
96
|
+
const prefix = opts.subscriptionPrefix ?? 'pubber';
|
|
97
|
+
const subName =
|
|
98
|
+
meta?.subscriptionName ??
|
|
99
|
+
`${prefix}-${sanitize(topic)}-${process.pid}-${Date.now()}-${++idCounter}`;
|
|
100
|
+
let subscription = c.subscription(subName);
|
|
101
|
+
|
|
102
|
+
if (meta?.createIfMissing) {
|
|
103
|
+
const [exists] = await subscription.exists();
|
|
104
|
+
if (!exists) {
|
|
105
|
+
try {
|
|
106
|
+
await c.topic(topic).createSubscription(subName, meta.subscriptionOptions);
|
|
107
|
+
} catch (err) {
|
|
108
|
+
throw new SubscriptionError(
|
|
109
|
+
`Failed to create subscription "${subName}" on topic "${topic}"`,
|
|
110
|
+
{ cause: err },
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
subscription = c.subscription(subName);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const onMessage = async (message: Message) => {
|
|
118
|
+
let payload: unknown = message.data;
|
|
119
|
+
try {
|
|
120
|
+
payload = codec.decode(message.data);
|
|
121
|
+
} catch {
|
|
122
|
+
// Leave as raw buffer.
|
|
123
|
+
}
|
|
124
|
+
let resolved = false;
|
|
125
|
+
const adapterMsg: AdapterMessage = {
|
|
126
|
+
topic,
|
|
127
|
+
payload,
|
|
128
|
+
raw: message,
|
|
129
|
+
meta: {
|
|
130
|
+
attributes: message.attributes,
|
|
131
|
+
messageId: message.id,
|
|
132
|
+
publishTime: message.publishTime,
|
|
133
|
+
orderingKey: message.orderingKey,
|
|
134
|
+
},
|
|
135
|
+
ack: async () => {
|
|
136
|
+
if (resolved) return;
|
|
137
|
+
resolved = true;
|
|
138
|
+
message.ack();
|
|
139
|
+
},
|
|
140
|
+
nack: async () => {
|
|
141
|
+
if (resolved) return;
|
|
142
|
+
resolved = true;
|
|
143
|
+
message.nack();
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
try {
|
|
147
|
+
await handler(adapterMsg);
|
|
148
|
+
if (!resolved) {
|
|
149
|
+
resolved = true;
|
|
150
|
+
message.ack();
|
|
151
|
+
}
|
|
152
|
+
} catch {
|
|
153
|
+
if (!resolved) {
|
|
154
|
+
resolved = true;
|
|
155
|
+
message.nack();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
subscription.on('message', onMessage);
|
|
161
|
+
liveSubscriptions.add(subscription);
|
|
162
|
+
|
|
163
|
+
idCounter += 1;
|
|
164
|
+
const id = `gcp-${idCounter}`;
|
|
165
|
+
return {
|
|
166
|
+
id,
|
|
167
|
+
topic,
|
|
168
|
+
unsubscribe: async () => {
|
|
169
|
+
subscription.off('message', onMessage);
|
|
170
|
+
await subscription.close().catch(noop);
|
|
171
|
+
liveSubscriptions.delete(subscription);
|
|
172
|
+
if (meta?.ephemeral) {
|
|
173
|
+
await subscription.delete().catch(noop);
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function sanitize(name: string): string {
|
|
182
|
+
// GCP subscription names must match [a-zA-Z][a-zA-Z0-9_\-.~+%]{2,254}
|
|
183
|
+
return name.replace(/[^a-zA-Z0-9_\-.~+%]/g, '-').slice(0, 80);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function noop(): void {
|
|
187
|
+
// intentional
|
|
188
|
+
}
|