@windrun-huaiin/backend-core 20.0.0 → 20.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/dist/index.js CHANGED
@@ -171,7 +171,9 @@ exports.smembers = redisStructures.smembers;
171
171
  exports.srem = redisStructures.srem;
172
172
  exports.ttl = redisStructures.ttl;
173
173
  exports.cancelSchedule = qstash.cancelSchedule;
174
+ exports.publishBroadcastMessage = qstash.publishBroadcastMessage;
174
175
  exports.publishDelayedMessage = qstash.publishDelayedMessage;
176
+ exports.publishFIFOQueueMessage = qstash.publishFIFOQueueMessage;
175
177
  exports.publishMessage = qstash.publishMessage;
176
178
  exports.scheduleMessage = qstash.scheduleMessage;
177
179
  exports.verifyQstashSignature = qstash.verifyQstashSignature;
package/dist/index.mjs CHANGED
@@ -32,4 +32,4 @@ export { getTargetLikeCount, getUserLikedTargets, isTargetLiked, likeTarget, unl
32
32
  export { addFavorite, getFavoriteCount, getUserFavorites, isFavorited, removeFavorite } from './lib/upstash/redis-favorite.mjs';
33
33
  export { getCounter, getUniqueCounter, incrCounter, incrUniqueCounter } from './lib/upstash/redis-counter.mjs';
34
34
  export { del, deleteHashField, deleteKey, exists, expire, getHashAll, getHashField, getHashJson, getJson, getString, hexists, hkeys, hlen, hmget, hmset, listLength, mget, mgetJson, mset, msetJson, pipeline, popList, pushList, rangeList, sadd, scard, setHashField, setHashJson, setJson, setString, sismember, smembers, srem, ttl } from './lib/upstash/redis-structures.mjs';
35
- export { cancelSchedule, publishDelayedMessage, publishMessage, scheduleMessage, verifyQstashSignature } from './lib/upstash/qstash.mjs';
35
+ export { cancelSchedule, publishBroadcastMessage, publishDelayedMessage, publishFIFOQueueMessage, publishMessage, scheduleMessage, verifyQstashSignature } from './lib/upstash/qstash.mjs';
package/dist/lib/index.js CHANGED
@@ -91,7 +91,9 @@ exports.smembers = redisStructures.smembers;
91
91
  exports.srem = redisStructures.srem;
92
92
  exports.ttl = redisStructures.ttl;
93
93
  exports.cancelSchedule = qstash.cancelSchedule;
94
+ exports.publishBroadcastMessage = qstash.publishBroadcastMessage;
94
95
  exports.publishDelayedMessage = qstash.publishDelayedMessage;
96
+ exports.publishFIFOQueueMessage = qstash.publishFIFOQueueMessage;
95
97
  exports.publishMessage = qstash.publishMessage;
96
98
  exports.scheduleMessage = qstash.scheduleMessage;
97
99
  exports.verifyQstashSignature = qstash.verifyQstashSignature;
@@ -9,4 +9,4 @@ export { getTargetLikeCount, getUserLikedTargets, isTargetLiked, likeTarget, unl
9
9
  export { addFavorite, getFavoriteCount, getUserFavorites, isFavorited, removeFavorite } from './upstash/redis-favorite.mjs';
10
10
  export { getCounter, getUniqueCounter, incrCounter, incrUniqueCounter } from './upstash/redis-counter.mjs';
11
11
  export { del, deleteHashField, deleteKey, exists, expire, getHashAll, getHashField, getHashJson, getJson, getString, hexists, hkeys, hlen, hmget, hmset, listLength, mget, mgetJson, mset, msetJson, pipeline, popList, pushList, rangeList, sadd, scard, setHashField, setHashJson, setJson, setString, sismember, smembers, srem, ttl } from './upstash/redis-structures.mjs';
12
- export { cancelSchedule, publishDelayedMessage, publishMessage, scheduleMessage, verifyQstashSignature } from './upstash/qstash.mjs';
12
+ export { cancelSchedule, publishBroadcastMessage, publishDelayedMessage, publishFIFOQueueMessage, publishMessage, scheduleMessage, verifyQstashSignature } from './upstash/qstash.mjs';
@@ -7,6 +7,15 @@ export interface PublishMessageOptions<TBody extends PublishBody = PublishBody>
7
7
  url: string;
8
8
  body: TBody;
9
9
  }
10
+ export interface PublishBroadcastMessageOptions<TBody extends PublishBody = PublishBody> {
11
+ urlGroup: string;
12
+ body: TBody;
13
+ }
14
+ export interface PublishFIFOQueueMessageOptions<TBody extends PublishBody = PublishBody> {
15
+ queueName: string;
16
+ url: string;
17
+ body: TBody;
18
+ }
10
19
  /**
11
20
  * Publish a message. Returns message id or null if QStash is unavailable.
12
21
  */
@@ -14,6 +23,22 @@ export declare const publishMessage: <TBody extends PublishBody>(options: Publis
14
23
  messageId: string | null;
15
24
  message: QstashEnvelope<TBody>;
16
25
  } | null>;
26
+ /**
27
+ * Publish a broadcast message to a QStash URL Group.
28
+ * Returns message ids or null if QStash is unavailable.
29
+ */
30
+ export declare const publishBroadcastMessage: <TBody extends PublishBody>(options: PublishBroadcastMessageOptions<TBody>) => Promise<{
31
+ messageIds: string[];
32
+ message: QstashEnvelope<TBody>;
33
+ } | null>;
34
+ /**
35
+ * Publish a single-recipient message into a QStash FIFO queue.
36
+ * Returns message id or null if QStash is unavailable.
37
+ */
38
+ export declare const publishFIFOQueueMessage: <TBody extends PublishBody>(options: PublishFIFOQueueMessageOptions<TBody>) => Promise<{
39
+ messageId: string | null;
40
+ message: QstashEnvelope<TBody>;
41
+ } | null>;
17
42
  /**
18
43
  * Publish a delayed message. Returns message id or null if QStash is unavailable.
19
44
  */
@@ -1 +1 @@
1
- {"version":3,"file":"qstash.d.ts","sourceRoot":"","sources":["../../../src/lib/upstash/qstash.ts"],"names":[],"mappings":"AA8CA,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,CAAC;AAErF,MAAM,WAAW,cAAc,CAAC,KAAK,SAAS,WAAW,GAAG,WAAW;IACrE,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,KAAK,CAAC;CAChB;AAED,MAAM,WAAW,qBAAqB,CAAC,KAAK,SAAS,WAAW,GAAG,WAAW;IAC5E,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,KAAK,CAAC;CACb;AAiBD;;GAEG;AACH,eAAO,MAAM,cAAc,GAAU,KAAK,SAAS,WAAW,EAC5D,SAAS,qBAAqB,CAAC,KAAK,CAAC,KACpC,OAAO,CAAC;IAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,OAAO,EAAE,cAAc,CAAC,KAAK,CAAC,CAAA;CAAE,GAAG,IAAI,CAa7E,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,qBAAqB,GAAU,KAAK,SAAS,WAAW,EACnE,SAAS,qBAAqB,CAAC,KAAK,CAAC,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,KAC3D,OAAO,CAAC;IAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,OAAO,EAAE,cAAc,CAAC,KAAK,CAAC,CAAA;CAAE,GAAG,IAAI,CAc7E,CAAC;AAEF,MAAM,WAAW,sBAAsB,CAAC,KAAK,SAAS,WAAW,GAAG,WAAW,CAC7E,SAAQ,qBAAqB,CAAC,KAAK,CAAC;IACpC,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;GAEG;AACH,eAAO,MAAM,eAAe,GAAU,KAAK,SAAS,WAAW,EAC7D,SAAS,sBAAsB,CAAC,KAAK,CAAC,KACrC,OAAO,CAAC;IAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,OAAO,EAAE,cAAc,CAAC,KAAK,CAAC,CAAA;CAAE,GAAG,IAAI,CAsB9E,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,cAAc,GAAU,YAAY,MAAM,KAAG,OAAO,CAAC,OAAO,CAexE,CAAC;AAEF,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;CACb;AAED;;GAEG;AACH,eAAO,MAAM,qBAAqB,GAAU,SAAS,mBAAmB,KAAG,OAAO,CAAC,OAAO,CAqBzF,CAAC"}
1
+ {"version":3,"file":"qstash.d.ts","sourceRoot":"","sources":["../../../src/lib/upstash/qstash.ts"],"names":[],"mappings":"AAgFA,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,CAAC;AAErF,MAAM,WAAW,cAAc,CAAC,KAAK,SAAS,WAAW,GAAG,WAAW;IACrE,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,KAAK,CAAC;CAChB;AAED,MAAM,WAAW,qBAAqB,CAAC,KAAK,SAAS,WAAW,GAAG,WAAW;IAC5E,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,KAAK,CAAC;CACb;AAED,MAAM,WAAW,8BAA8B,CAAC,KAAK,SAAS,WAAW,GAAG,WAAW;IACrF,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,KAAK,CAAC;CACb;AAED,MAAM,WAAW,8BAA8B,CAAC,KAAK,SAAS,WAAW,GAAG,WAAW;IACrF,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,KAAK,CAAC;CACb;AAiBD;;GAEG;AACH,eAAO,MAAM,cAAc,GAAU,KAAK,SAAS,WAAW,EAC5D,SAAS,qBAAqB,CAAC,KAAK,CAAC,KACpC,OAAO,CAAC;IAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,OAAO,EAAE,cAAc,CAAC,KAAK,CAAC,CAAA;CAAE,GAAG,IAAI,CAa7E,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,uBAAuB,GAAU,KAAK,SAAS,WAAW,EACrE,SAAS,8BAA8B,CAAC,KAAK,CAAC,KAC7C,OAAO,CAAC;IAAE,UAAU,EAAE,MAAM,EAAE,CAAC;IAAC,OAAO,EAAE,cAAc,CAAC,KAAK,CAAC,CAAA;CAAE,GAAG,IAAI,CA0BzE,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,uBAAuB,GAAU,KAAK,SAAS,WAAW,EACrE,SAAS,8BAA8B,CAAC,KAAK,CAAC,KAC7C,OAAO,CAAC;IAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,OAAO,EAAE,cAAc,CAAC,KAAK,CAAC,CAAA;CAAE,GAAG,IAAI,CAqB7E,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,qBAAqB,GAAU,KAAK,SAAS,WAAW,EACnE,SAAS,qBAAqB,CAAC,KAAK,CAAC,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,KAC3D,OAAO,CAAC;IAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,OAAO,EAAE,cAAc,CAAC,KAAK,CAAC,CAAA;CAAE,GAAG,IAAI,CAc7E,CAAC;AAEF,MAAM,WAAW,sBAAsB,CAAC,KAAK,SAAS,WAAW,GAAG,WAAW,CAC7E,SAAQ,qBAAqB,CAAC,KAAK,CAAC;IACpC,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;GAEG;AACH,eAAO,MAAM,eAAe,GAAU,KAAK,SAAS,WAAW,EAC7D,SAAS,sBAAsB,CAAC,KAAK,CAAC,KACrC,OAAO,CAAC;IAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,OAAO,EAAE,cAAc,CAAC,KAAK,CAAC,CAAA;CAAE,GAAG,IAAI,CAsB9E,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,cAAc,GAAU,YAAY,MAAM,KAAG,OAAO,CAAC,OAAO,CAexE,CAAC;AAEF,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;CACb;AAED;;GAEG;AACH,eAAO,MAAM,qBAAqB,GAAU,SAAS,mBAAmB,KAAG,OAAO,CAAC,OAAO,CAqBzF,CAAC"}
@@ -7,8 +7,30 @@ var upstashConfig = require('../upstash-config.js');
7
7
  let cachedReceiver = null;
8
8
  let receiverWarnedMissingEnv = false;
9
9
  let receiverWarnedInitError = false;
10
+ const isNonEmpty = (value) => typeof value === 'string' && value.trim().length > 0;
10
11
  const isTruthy = (value) => value === '1' || value === 'true' || value === 'TRUE';
11
12
  const shouldSkipVerify = () => process.env.NODE_ENV === 'development' && isTruthy(process.env.SKIP_UPSTASH_QSTASH_VERIFY);
13
+ const getRequiredAppName = () => {
14
+ const appName = process.env.NEXT_PUBLIC_APP_NAME;
15
+ if (!isNonEmpty(appName)) {
16
+ throw new Error('[Upstash QStash] NEXT_PUBLIC_APP_NAME is required for QStash naming and must not be empty');
17
+ }
18
+ const normalized = appName.replace(/\s+/g, '').toLowerCase();
19
+ if (!normalized) {
20
+ throw new Error('[Upstash QStash] NEXT_PUBLIC_APP_NAME must contain non-whitespace characters for QStash naming');
21
+ }
22
+ return normalized;
23
+ };
24
+ const getQstashNamePrefix = () => {
25
+ const envSuffix = process.env.NODE_ENV === 'production' ? 'live' : 'test';
26
+ return `${getRequiredAppName()}_${envSuffix}`;
27
+ };
28
+ const prefixQstashName = (resourceType, name) => {
29
+ if (!isNonEmpty(name)) {
30
+ throw new Error(`[Upstash QStash] ${resourceType} name must not be empty`);
31
+ }
32
+ return `${getQstashNamePrefix()}_${resourceType}_${name}`;
33
+ };
12
34
  const getReceiver = () => {
13
35
  if (cachedReceiver) {
14
36
  return cachedReceiver;
@@ -69,6 +91,56 @@ const publishMessage = (options) => tslib.__awaiter(void 0, void 0, void 0, func
69
91
  };
70
92
  }));
71
93
  });
94
+ /**
95
+ * Publish a broadcast message to a QStash URL Group.
96
+ * Returns message ids or null if QStash is unavailable.
97
+ */
98
+ const publishBroadcastMessage = (options) => tslib.__awaiter(void 0, void 0, void 0, function* () {
99
+ const message = createEnvelope(options.body);
100
+ return upstashConfig.withQstash((client) => tslib.__awaiter(void 0, void 0, void 0, function* () {
101
+ const result = yield client.publishJSON({
102
+ urlGroup: options.urlGroup,
103
+ body: message,
104
+ });
105
+ const messageIds = Array.isArray(result)
106
+ ? result
107
+ .map((item) => typeof item === 'string' ? item : typeof (item === null || item === void 0 ? void 0 : item.messageId) === 'string' ? item.messageId : null)
108
+ .filter((messageId) => typeof messageId === 'string')
109
+ : typeof result === 'string'
110
+ ? [result]
111
+ : typeof (result === null || result === void 0 ? void 0 : result.messageId) === 'string'
112
+ ? [result.messageId]
113
+ : [];
114
+ return {
115
+ messageIds,
116
+ message,
117
+ };
118
+ }));
119
+ });
120
+ /**
121
+ * Publish a single-recipient message into a QStash FIFO queue.
122
+ * Returns message id or null if QStash is unavailable.
123
+ */
124
+ const publishFIFOQueueMessage = (options) => tslib.__awaiter(void 0, void 0, void 0, function* () {
125
+ const message = createEnvelope(options.body);
126
+ const queueName = prefixQstashName('queue', options.queueName);
127
+ return upstashConfig.withQstash((client) => tslib.__awaiter(void 0, void 0, void 0, function* () {
128
+ var _a, _b;
129
+ const anyClient = client;
130
+ const queueClient = (_a = anyClient.queue) === null || _a === void 0 ? void 0 : _a.call(anyClient, { queueName });
131
+ if (!(queueClient === null || queueClient === void 0 ? void 0 : queueClient.enqueueJSON)) {
132
+ throw new Error('QStash queue enqueueJSON API is unavailable');
133
+ }
134
+ const result = yield queueClient.enqueueJSON({
135
+ url: options.url,
136
+ body: message,
137
+ });
138
+ return {
139
+ messageId: typeof result === 'string' ? result : (_b = result === null || result === void 0 ? void 0 : result.messageId) !== null && _b !== void 0 ? _b : null,
140
+ message,
141
+ };
142
+ }));
143
+ });
72
144
  /**
73
145
  * Publish a delayed message. Returns message id or null if QStash is unavailable.
74
146
  */
@@ -154,7 +226,9 @@ const verifyQstashSignature = (options) => tslib.__awaiter(void 0, void 0, void
154
226
  });
155
227
 
156
228
  exports.cancelSchedule = cancelSchedule;
229
+ exports.publishBroadcastMessage = publishBroadcastMessage;
157
230
  exports.publishDelayedMessage = publishDelayedMessage;
231
+ exports.publishFIFOQueueMessage = publishFIFOQueueMessage;
158
232
  exports.publishMessage = publishMessage;
159
233
  exports.scheduleMessage = scheduleMessage;
160
234
  exports.verifyQstashSignature = verifyQstashSignature;
@@ -5,8 +5,30 @@ import { withQstash } from '../upstash-config.mjs';
5
5
  let cachedReceiver = null;
6
6
  let receiverWarnedMissingEnv = false;
7
7
  let receiverWarnedInitError = false;
8
+ const isNonEmpty = (value) => typeof value === 'string' && value.trim().length > 0;
8
9
  const isTruthy = (value) => value === '1' || value === 'true' || value === 'TRUE';
9
10
  const shouldSkipVerify = () => process.env.NODE_ENV === 'development' && isTruthy(process.env.SKIP_UPSTASH_QSTASH_VERIFY);
11
+ const getRequiredAppName = () => {
12
+ const appName = process.env.NEXT_PUBLIC_APP_NAME;
13
+ if (!isNonEmpty(appName)) {
14
+ throw new Error('[Upstash QStash] NEXT_PUBLIC_APP_NAME is required for QStash naming and must not be empty');
15
+ }
16
+ const normalized = appName.replace(/\s+/g, '').toLowerCase();
17
+ if (!normalized) {
18
+ throw new Error('[Upstash QStash] NEXT_PUBLIC_APP_NAME must contain non-whitespace characters for QStash naming');
19
+ }
20
+ return normalized;
21
+ };
22
+ const getQstashNamePrefix = () => {
23
+ const envSuffix = process.env.NODE_ENV === 'production' ? 'live' : 'test';
24
+ return `${getRequiredAppName()}_${envSuffix}`;
25
+ };
26
+ const prefixQstashName = (resourceType, name) => {
27
+ if (!isNonEmpty(name)) {
28
+ throw new Error(`[Upstash QStash] ${resourceType} name must not be empty`);
29
+ }
30
+ return `${getQstashNamePrefix()}_${resourceType}_${name}`;
31
+ };
10
32
  const getReceiver = () => {
11
33
  if (cachedReceiver) {
12
34
  return cachedReceiver;
@@ -67,6 +89,56 @@ const publishMessage = (options) => __awaiter(void 0, void 0, void 0, function*
67
89
  };
68
90
  }));
69
91
  });
92
+ /**
93
+ * Publish a broadcast message to a QStash URL Group.
94
+ * Returns message ids or null if QStash is unavailable.
95
+ */
96
+ const publishBroadcastMessage = (options) => __awaiter(void 0, void 0, void 0, function* () {
97
+ const message = createEnvelope(options.body);
98
+ return withQstash((client) => __awaiter(void 0, void 0, void 0, function* () {
99
+ const result = yield client.publishJSON({
100
+ urlGroup: options.urlGroup,
101
+ body: message,
102
+ });
103
+ const messageIds = Array.isArray(result)
104
+ ? result
105
+ .map((item) => typeof item === 'string' ? item : typeof (item === null || item === void 0 ? void 0 : item.messageId) === 'string' ? item.messageId : null)
106
+ .filter((messageId) => typeof messageId === 'string')
107
+ : typeof result === 'string'
108
+ ? [result]
109
+ : typeof (result === null || result === void 0 ? void 0 : result.messageId) === 'string'
110
+ ? [result.messageId]
111
+ : [];
112
+ return {
113
+ messageIds,
114
+ message,
115
+ };
116
+ }));
117
+ });
118
+ /**
119
+ * Publish a single-recipient message into a QStash FIFO queue.
120
+ * Returns message id or null if QStash is unavailable.
121
+ */
122
+ const publishFIFOQueueMessage = (options) => __awaiter(void 0, void 0, void 0, function* () {
123
+ const message = createEnvelope(options.body);
124
+ const queueName = prefixQstashName('queue', options.queueName);
125
+ return withQstash((client) => __awaiter(void 0, void 0, void 0, function* () {
126
+ var _a, _b;
127
+ const anyClient = client;
128
+ const queueClient = (_a = anyClient.queue) === null || _a === void 0 ? void 0 : _a.call(anyClient, { queueName });
129
+ if (!(queueClient === null || queueClient === void 0 ? void 0 : queueClient.enqueueJSON)) {
130
+ throw new Error('QStash queue enqueueJSON API is unavailable');
131
+ }
132
+ const result = yield queueClient.enqueueJSON({
133
+ url: options.url,
134
+ body: message,
135
+ });
136
+ return {
137
+ messageId: typeof result === 'string' ? result : (_b = result === null || result === void 0 ? void 0 : result.messageId) !== null && _b !== void 0 ? _b : null,
138
+ message,
139
+ };
140
+ }));
141
+ });
70
142
  /**
71
143
  * Publish a delayed message. Returns message id or null if QStash is unavailable.
72
144
  */
@@ -151,4 +223,4 @@ const verifyQstashSignature = (options) => __awaiter(void 0, void 0, void 0, fun
151
223
  return true;
152
224
  });
153
225
 
154
- export { cancelSchedule, publishDelayedMessage, publishMessage, scheduleMessage, verifyQstashSignature };
226
+ export { cancelSchedule, publishBroadcastMessage, publishDelayedMessage, publishFIFOQueueMessage, publishMessage, scheduleMessage, verifyQstashSignature };
@@ -1 +1 @@
1
- {"version":3,"file":"upstash-config.d.ts","sourceRoot":"","sources":["../../src/lib/upstash-config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AACvC,OAAO,EAAE,MAAM,IAAI,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAwOzD;;;;;GAKG;AACH,eAAO,MAAM,QAAQ,QAAO,KAAK,GAAG,IAEnC,CAAC;AAwDF;;;;;GAKG;AACH,eAAO,MAAM,SAAS,QAAO,YAAY,GAAG,IAE3C,CAAC;AAyCF,eAAO,MAAM,SAAS,GAAU,CAAC,EAAE,IAAI,CAAC,KAAK,EAAE,KAAK,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,KAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAMzF,CAAC;AAEF,eAAO,MAAM,UAAU,GAAU,CAAC,EAChC,IAAI,CAAC,MAAM,EAAE,YAAY,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,KAC3C,OAAO,CAAC,CAAC,GAAG,IAAI,CAMlB,CAAC"}
1
+ {"version":3,"file":"upstash-config.d.ts","sourceRoot":"","sources":["../../src/lib/upstash-config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AACvC,OAAO,EAAE,MAAM,IAAI,YAAY,EAAE,MAAM,iBAAiB,CAAC;AA8OzD;;;;;GAKG;AACH,eAAO,MAAM,QAAQ,QAAO,KAAK,GAAG,IAEnC,CAAC;AAwDF;;;;;GAKG;AACH,eAAO,MAAM,SAAS,QAAO,YAAY,GAAG,IAE3C,CAAC;AA+CF,eAAO,MAAM,SAAS,GAAU,CAAC,EAAE,IAAI,CAAC,KAAK,EAAE,KAAK,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,KAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAMzF,CAAC;AAEF,eAAO,MAAM,UAAU,GAAU,CAAC,EAChC,IAAI,CAAC,MAAM,EAAE,YAAY,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,KAC3C,OAAO,CAAC,CAAC,GAAG,IAAI,CAMlB,CAAC"}
@@ -111,7 +111,10 @@ const parseMinutes = (value, fallback) => {
111
111
  };
112
112
  const getRedisHealthIntervalMinutes = () => parseMinutes(process.env.UPSTASH_REDIS_HEALTHCHECK_INTERVAL_MINUTES, 10);
113
113
  const getQstashHealthIntervalMinutes = () => parseMinutes(process.env.UPSTASH_QSTASH_HEALTHCHECK_INTERVAL_MINUTES, 10);
114
- const getQstashHealthcheckUrl = () => { var _a; return (_a = process.env.UPSTASH_QSTASH_HEALTHCHECK_URL) !== null && _a !== void 0 ? _a : 'https://qstash.upstash.io/v2/topics'; };
114
+ const getQstashHealthcheckUrl = () => {
115
+ const url = process.env.UPSTASH_QSTASH_HEALTHCHECK_URL;
116
+ return isNonEmpty(url) ? url : null;
117
+ };
115
118
  const scheduleRedisHealthCheck = () => {
116
119
  if (redisHealthTimer || !cachedRedis) {
117
120
  return;
@@ -148,7 +151,11 @@ const scheduleRedisHealthCheck = () => {
148
151
  }), delayMs);
149
152
  };
150
153
  const checkQstashHealth = (token) => tslib.__awaiter(void 0, void 0, void 0, function* () {
151
- const response = yield fetch(getQstashHealthcheckUrl(), {
154
+ const healthcheckUrl = getQstashHealthcheckUrl();
155
+ if (!healthcheckUrl) {
156
+ return;
157
+ }
158
+ const response = yield fetch(healthcheckUrl, {
152
159
  method: 'GET',
153
160
  headers: {
154
161
  Authorization: `Bearer ${token}`,
@@ -160,7 +167,7 @@ const checkQstashHealth = (token) => tslib.__awaiter(void 0, void 0, void 0, fun
160
167
  }
161
168
  });
162
169
  const scheduleQstashHealthCheck = (token) => {
163
- if (qstashHealthTimer || !cachedQstash) {
170
+ if (qstashHealthTimer || !cachedQstash || !getQstashHealthcheckUrl()) {
164
171
  return;
165
172
  }
166
173
  const delayMs = getQstashHealthIntervalMinutes() * 60000;
@@ -173,7 +180,6 @@ const scheduleQstashHealthCheck = (token) => {
173
180
  yield checkQstashHealth(token);
174
181
  }
175
182
  catch (error) {
176
- cachedQstash = null;
177
183
  if (!qstashWarnedHealthCheck) {
178
184
  qstashWarnedHealthCheck = true;
179
185
  const message = error instanceof Error ? error.message : String(error);
@@ -279,8 +285,14 @@ const ensureQstash = () => tslib.__awaiter(void 0, void 0, void 0, function* ()
279
285
  }
280
286
  try {
281
287
  const client = new qstash.Client({ token: QSTASH_TOKEN });
282
- yield checkQstashHealth(QSTASH_TOKEN);
283
288
  cachedQstash = client;
289
+ checkQstashHealth(QSTASH_TOKEN).catch((error) => {
290
+ if (!qstashWarnedHealthCheck) {
291
+ qstashWarnedHealthCheck = true;
292
+ const message = error instanceof Error ? error.message : String(error);
293
+ console.warn(`[Upstash Config] QStash health check failed: ${message}`);
294
+ }
295
+ });
284
296
  scheduleQstashHealthCheck(QSTASH_TOKEN);
285
297
  return cachedQstash;
286
298
  }
@@ -109,7 +109,10 @@ const parseMinutes = (value, fallback) => {
109
109
  };
110
110
  const getRedisHealthIntervalMinutes = () => parseMinutes(process.env.UPSTASH_REDIS_HEALTHCHECK_INTERVAL_MINUTES, 10);
111
111
  const getQstashHealthIntervalMinutes = () => parseMinutes(process.env.UPSTASH_QSTASH_HEALTHCHECK_INTERVAL_MINUTES, 10);
112
- const getQstashHealthcheckUrl = () => { var _a; return (_a = process.env.UPSTASH_QSTASH_HEALTHCHECK_URL) !== null && _a !== void 0 ? _a : 'https://qstash.upstash.io/v2/topics'; };
112
+ const getQstashHealthcheckUrl = () => {
113
+ const url = process.env.UPSTASH_QSTASH_HEALTHCHECK_URL;
114
+ return isNonEmpty(url) ? url : null;
115
+ };
113
116
  const scheduleRedisHealthCheck = () => {
114
117
  if (redisHealthTimer || !cachedRedis) {
115
118
  return;
@@ -146,7 +149,11 @@ const scheduleRedisHealthCheck = () => {
146
149
  }), delayMs);
147
150
  };
148
151
  const checkQstashHealth = (token) => __awaiter(void 0, void 0, void 0, function* () {
149
- const response = yield fetch(getQstashHealthcheckUrl(), {
152
+ const healthcheckUrl = getQstashHealthcheckUrl();
153
+ if (!healthcheckUrl) {
154
+ return;
155
+ }
156
+ const response = yield fetch(healthcheckUrl, {
150
157
  method: 'GET',
151
158
  headers: {
152
159
  Authorization: `Bearer ${token}`,
@@ -158,7 +165,7 @@ const checkQstashHealth = (token) => __awaiter(void 0, void 0, void 0, function*
158
165
  }
159
166
  });
160
167
  const scheduleQstashHealthCheck = (token) => {
161
- if (qstashHealthTimer || !cachedQstash) {
168
+ if (qstashHealthTimer || !cachedQstash || !getQstashHealthcheckUrl()) {
162
169
  return;
163
170
  }
164
171
  const delayMs = getQstashHealthIntervalMinutes() * 60000;
@@ -171,7 +178,6 @@ const scheduleQstashHealthCheck = (token) => {
171
178
  yield checkQstashHealth(token);
172
179
  }
173
180
  catch (error) {
174
- cachedQstash = null;
175
181
  if (!qstashWarnedHealthCheck) {
176
182
  qstashWarnedHealthCheck = true;
177
183
  const message = error instanceof Error ? error.message : String(error);
@@ -277,8 +283,14 @@ const ensureQstash = () => __awaiter(void 0, void 0, void 0, function* () {
277
283
  }
278
284
  try {
279
285
  const client = new Client({ token: QSTASH_TOKEN });
280
- yield checkQstashHealth(QSTASH_TOKEN);
281
286
  cachedQstash = client;
287
+ checkQstashHealth(QSTASH_TOKEN).catch((error) => {
288
+ if (!qstashWarnedHealthCheck) {
289
+ qstashWarnedHealthCheck = true;
290
+ const message = error instanceof Error ? error.message : String(error);
291
+ console.warn(`[Upstash Config] QStash health check failed: ${message}`);
292
+ }
293
+ });
282
294
  scheduleQstashHealthCheck(QSTASH_TOKEN);
283
295
  return cachedQstash;
284
296
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windrun-huaiin/backend-core",
3
- "version": "20.0.0",
3
+ "version": "20.0.1",
4
4
  "description": "Shared backend primitives: Prisma schema/client, database services, routing helpers",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -113,8 +113,8 @@
113
113
  "tslib": "^2.8.1",
114
114
  "zod": "^4.3.6",
115
115
  "@windrun-huaiin/contracts": "^20.0.0",
116
- "@windrun-huaiin/lib": "^20.0.0",
117
- "@windrun-huaiin/third-ui": "^20.0.0"
116
+ "@windrun-huaiin/third-ui": "^20.0.0",
117
+ "@windrun-huaiin/lib": "^20.0.0"
118
118
  },
119
119
  "devDependencies": {
120
120
  "@rollup/plugin-alias": "^5.1.1",
@@ -5,12 +5,46 @@ let cachedReceiver: Receiver | null = null;
5
5
  let receiverWarnedMissingEnv = false;
6
6
  let receiverWarnedInitError = false;
7
7
 
8
+ const isNonEmpty = (value: string | undefined): value is string =>
9
+ typeof value === 'string' && value.trim().length > 0;
10
+
8
11
  const isTruthy = (value: string | undefined): boolean =>
9
12
  value === '1' || value === 'true' || value === 'TRUE';
10
13
 
11
14
  const shouldSkipVerify = (): boolean =>
12
15
  process.env.NODE_ENV === 'development' && isTruthy(process.env.SKIP_UPSTASH_QSTASH_VERIFY);
13
16
 
17
+ const getRequiredAppName = (): string => {
18
+ const appName = process.env.NEXT_PUBLIC_APP_NAME;
19
+ if (!isNonEmpty(appName)) {
20
+ throw new Error(
21
+ '[Upstash QStash] NEXT_PUBLIC_APP_NAME is required for QStash naming and must not be empty'
22
+ );
23
+ }
24
+
25
+ const normalized = appName.replace(/\s+/g, '').toLowerCase();
26
+ if (!normalized) {
27
+ throw new Error(
28
+ '[Upstash QStash] NEXT_PUBLIC_APP_NAME must contain non-whitespace characters for QStash naming'
29
+ );
30
+ }
31
+
32
+ return normalized;
33
+ };
34
+
35
+ const getQstashNamePrefix = (): string => {
36
+ const envSuffix = process.env.NODE_ENV === 'production' ? 'live' : 'test';
37
+ return `${getRequiredAppName()}_${envSuffix}`;
38
+ };
39
+
40
+ const prefixQstashName = (resourceType: 'queue', name: string): string => {
41
+ if (!isNonEmpty(name)) {
42
+ throw new Error(`[Upstash QStash] ${resourceType} name must not be empty`);
43
+ }
44
+
45
+ return `${getQstashNamePrefix()}_${resourceType}_${name}`;
46
+ };
47
+
14
48
  const getReceiver = (): Receiver | null => {
15
49
  if (cachedReceiver) {
16
50
  return cachedReceiver;
@@ -56,6 +90,17 @@ export interface PublishMessageOptions<TBody extends PublishBody = PublishBody>
56
90
  body: TBody;
57
91
  }
58
92
 
93
+ export interface PublishBroadcastMessageOptions<TBody extends PublishBody = PublishBody> {
94
+ urlGroup: string;
95
+ body: TBody;
96
+ }
97
+
98
+ export interface PublishFIFOQueueMessageOptions<TBody extends PublishBody = PublishBody> {
99
+ queueName: string;
100
+ url: string;
101
+ body: TBody;
102
+ }
103
+
59
104
  const generateSourceMessageId = (): string => {
60
105
  try {
61
106
  return crypto.randomUUID();
@@ -91,6 +136,69 @@ export const publishMessage = async <TBody extends PublishBody>(
91
136
  });
92
137
  };
93
138
 
139
+ /**
140
+ * Publish a broadcast message to a QStash URL Group.
141
+ * Returns message ids or null if QStash is unavailable.
142
+ */
143
+ export const publishBroadcastMessage = async <TBody extends PublishBody>(
144
+ options: PublishBroadcastMessageOptions<TBody>
145
+ ): Promise<{ messageIds: string[]; message: QstashEnvelope<TBody> } | null> => {
146
+ const message = createEnvelope(options.body);
147
+
148
+ return withQstash(async (client) => {
149
+ const result = await (client as any).publishJSON({
150
+ urlGroup: options.urlGroup,
151
+ body: message,
152
+ });
153
+
154
+ const messageIds = Array.isArray(result)
155
+ ? result
156
+ .map((item) =>
157
+ typeof item === 'string' ? item : typeof item?.messageId === 'string' ? item.messageId : null
158
+ )
159
+ .filter((messageId): messageId is string => typeof messageId === 'string')
160
+ : typeof result === 'string'
161
+ ? [result]
162
+ : typeof result?.messageId === 'string'
163
+ ? [result.messageId]
164
+ : [];
165
+
166
+ return {
167
+ messageIds,
168
+ message,
169
+ };
170
+ });
171
+ };
172
+
173
+ /**
174
+ * Publish a single-recipient message into a QStash FIFO queue.
175
+ * Returns message id or null if QStash is unavailable.
176
+ */
177
+ export const publishFIFOQueueMessage = async <TBody extends PublishBody>(
178
+ options: PublishFIFOQueueMessageOptions<TBody>
179
+ ): Promise<{ messageId: string | null; message: QstashEnvelope<TBody> } | null> => {
180
+ const message = createEnvelope(options.body);
181
+ const queueName = prefixQstashName('queue', options.queueName);
182
+
183
+ return withQstash(async (client) => {
184
+ const anyClient = client as any;
185
+ const queueClient = anyClient.queue?.({ queueName });
186
+ if (!queueClient?.enqueueJSON) {
187
+ throw new Error('QStash queue enqueueJSON API is unavailable');
188
+ }
189
+
190
+ const result = await queueClient.enqueueJSON({
191
+ url: options.url,
192
+ body: message,
193
+ });
194
+
195
+ return {
196
+ messageId: typeof result === 'string' ? result : result?.messageId ?? null,
197
+ message,
198
+ };
199
+ });
200
+ };
201
+
94
202
  /**
95
203
  * Publish a delayed message. Returns message id or null if QStash is unavailable.
96
204
  */
@@ -149,8 +149,10 @@ const getRedisHealthIntervalMinutes = (): number =>
149
149
  const getQstashHealthIntervalMinutes = (): number =>
150
150
  parseMinutes(process.env.UPSTASH_QSTASH_HEALTHCHECK_INTERVAL_MINUTES, 10);
151
151
 
152
- const getQstashHealthcheckUrl = (): string =>
153
- process.env.UPSTASH_QSTASH_HEALTHCHECK_URL ?? 'https://qstash.upstash.io/v2/topics';
152
+ const getQstashHealthcheckUrl = (): string | null => {
153
+ const url = process.env.UPSTASH_QSTASH_HEALTHCHECK_URL;
154
+ return isNonEmpty(url) ? url : null;
155
+ };
154
156
 
155
157
  const scheduleRedisHealthCheck = (): void => {
156
158
  if (redisHealthTimer || !cachedRedis) {
@@ -186,7 +188,12 @@ const scheduleRedisHealthCheck = (): void => {
186
188
  };
187
189
 
188
190
  const checkQstashHealth = async (token: string): Promise<void> => {
189
- const response = await fetch(getQstashHealthcheckUrl(), {
191
+ const healthcheckUrl = getQstashHealthcheckUrl();
192
+ if (!healthcheckUrl) {
193
+ return;
194
+ }
195
+
196
+ const response = await fetch(healthcheckUrl, {
190
197
  method: 'GET',
191
198
  headers: {
192
199
  Authorization: `Bearer ${token}`,
@@ -199,7 +206,7 @@ const checkQstashHealth = async (token: string): Promise<void> => {
199
206
  };
200
207
 
201
208
  const scheduleQstashHealthCheck = (token: string): void => {
202
- if (qstashHealthTimer || !cachedQstash) {
209
+ if (qstashHealthTimer || !cachedQstash || !getQstashHealthcheckUrl()) {
203
210
  return;
204
211
  }
205
212
  const delayMs = getQstashHealthIntervalMinutes() * 60_000;
@@ -211,7 +218,6 @@ const scheduleQstashHealthCheck = (token: string): void => {
211
218
  try {
212
219
  await checkQstashHealth(token);
213
220
  } catch (error) {
214
- cachedQstash = null;
215
221
  if (!qstashWarnedHealthCheck) {
216
222
  qstashWarnedHealthCheck = true;
217
223
  const message = error instanceof Error ? error.message : String(error);
@@ -325,8 +331,14 @@ const ensureQstash = async (): Promise<QstashClient | null> => {
325
331
 
326
332
  try {
327
333
  const client = new QstashClient({ token: QSTASH_TOKEN });
328
- await checkQstashHealth(QSTASH_TOKEN);
329
334
  cachedQstash = client;
335
+ checkQstashHealth(QSTASH_TOKEN).catch((error) => {
336
+ if (!qstashWarnedHealthCheck) {
337
+ qstashWarnedHealthCheck = true;
338
+ const message = error instanceof Error ? error.message : String(error);
339
+ console.warn(`[Upstash Config] QStash health check failed: ${message}`);
340
+ }
341
+ });
330
342
  scheduleQstashHealthCheck(QSTASH_TOKEN);
331
343
  return cachedQstash;
332
344
  } catch (error) {