@windrun-huaiin/backend-core 29.0.2 → 29.0.3

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.
@@ -67,7 +67,7 @@ const publishMessage = (options) => tslib.__awaiter(void 0, void 0, void 0, func
67
67
  messageId: typeof result === 'string' ? result : (_a = result === null || result === void 0 ? void 0 : result.messageId) !== null && _a !== void 0 ? _a : null,
68
68
  message,
69
69
  };
70
- }));
70
+ }), 'publishMessage');
71
71
  });
72
72
  /**
73
73
  * Publish a broadcast message to a QStash URL Group.
@@ -93,7 +93,7 @@ const publishBroadcastMessage = (options) => tslib.__awaiter(void 0, void 0, voi
93
93
  messageIds,
94
94
  message,
95
95
  };
96
- }));
96
+ }), 'publishBroadcastMessage');
97
97
  });
98
98
  /**
99
99
  * Publish a single-recipient message into a QStash FIFO queue.
@@ -117,7 +117,7 @@ const publishFIFOQueueMessage = (options) => tslib.__awaiter(void 0, void 0, voi
117
117
  messageId: typeof result === 'string' ? result : (_b = result === null || result === void 0 ? void 0 : result.messageId) !== null && _b !== void 0 ? _b : null,
118
118
  message,
119
119
  };
120
- }));
120
+ }), 'publishFIFOQueueMessage');
121
121
  });
122
122
  /**
123
123
  * Publish a delayed message. Returns message id or null if QStash is unavailable.
@@ -135,7 +135,7 @@ const publishDelayedMessage = (options) => tslib.__awaiter(void 0, void 0, void
135
135
  messageId: typeof result === 'string' ? result : (_a = result === null || result === void 0 ? void 0 : result.messageId) !== null && _a !== void 0 ? _a : null,
136
136
  message,
137
137
  };
138
- }));
138
+ }), 'publishDelayedMessage');
139
139
  });
140
140
  /**
141
141
  * Schedule a recurring message. Returns schedule id or null if QStash is unavailable.
@@ -158,7 +158,7 @@ const scheduleMessage = (options) => tslib.__awaiter(void 0, void 0, void 0, fun
158
158
  scheduleId: typeof result === 'string' ? result : (_f = (_e = result === null || result === void 0 ? void 0 : result.scheduleId) !== null && _e !== void 0 ? _e : result === null || result === void 0 ? void 0 : result.id) !== null && _f !== void 0 ? _f : null,
159
159
  message,
160
160
  };
161
- }));
161
+ }), 'scheduleMessage');
162
162
  });
163
163
  /**
164
164
  * Cancel a scheduled message. Returns false if QStash is unavailable.
@@ -176,7 +176,7 @@ const cancelSchedule = (scheduleId) => tslib.__awaiter(void 0, void 0, void 0, f
176
176
  return true;
177
177
  }
178
178
  return false;
179
- }));
179
+ }), 'cancelSchedule');
180
180
  return result !== null && result !== void 0 ? result : false;
181
181
  });
182
182
  /**
@@ -65,7 +65,7 @@ const publishMessage = (options) => __awaiter(void 0, void 0, void 0, function*
65
65
  messageId: typeof result === 'string' ? result : (_a = result === null || result === void 0 ? void 0 : result.messageId) !== null && _a !== void 0 ? _a : null,
66
66
  message,
67
67
  };
68
- }));
68
+ }), 'publishMessage');
69
69
  });
70
70
  /**
71
71
  * Publish a broadcast message to a QStash URL Group.
@@ -91,7 +91,7 @@ const publishBroadcastMessage = (options) => __awaiter(void 0, void 0, void 0, f
91
91
  messageIds,
92
92
  message,
93
93
  };
94
- }));
94
+ }), 'publishBroadcastMessage');
95
95
  });
96
96
  /**
97
97
  * Publish a single-recipient message into a QStash FIFO queue.
@@ -115,7 +115,7 @@ const publishFIFOQueueMessage = (options) => __awaiter(void 0, void 0, void 0, f
115
115
  messageId: typeof result === 'string' ? result : (_b = result === null || result === void 0 ? void 0 : result.messageId) !== null && _b !== void 0 ? _b : null,
116
116
  message,
117
117
  };
118
- }));
118
+ }), 'publishFIFOQueueMessage');
119
119
  });
120
120
  /**
121
121
  * Publish a delayed message. Returns message id or null if QStash is unavailable.
@@ -133,7 +133,7 @@ const publishDelayedMessage = (options) => __awaiter(void 0, void 0, void 0, fun
133
133
  messageId: typeof result === 'string' ? result : (_a = result === null || result === void 0 ? void 0 : result.messageId) !== null && _a !== void 0 ? _a : null,
134
134
  message,
135
135
  };
136
- }));
136
+ }), 'publishDelayedMessage');
137
137
  });
138
138
  /**
139
139
  * Schedule a recurring message. Returns schedule id or null if QStash is unavailable.
@@ -156,7 +156,7 @@ const scheduleMessage = (options) => __awaiter(void 0, void 0, void 0, function*
156
156
  scheduleId: typeof result === 'string' ? result : (_f = (_e = result === null || result === void 0 ? void 0 : result.scheduleId) !== null && _e !== void 0 ? _e : result === null || result === void 0 ? void 0 : result.id) !== null && _f !== void 0 ? _f : null,
157
157
  message,
158
158
  };
159
- }));
159
+ }), 'scheduleMessage');
160
160
  });
161
161
  /**
162
162
  * Cancel a scheduled message. Returns false if QStash is unavailable.
@@ -174,7 +174,7 @@ const cancelSchedule = (scheduleId) => __awaiter(void 0, void 0, void 0, functio
174
174
  return true;
175
175
  }
176
176
  return false;
177
- }));
177
+ }), 'cancelSchedule');
178
178
  return result !== null && result !== void 0 ? result : false;
179
179
  });
180
180
  /**
@@ -19,5 +19,5 @@ export declare const getRedis: () => Redis | null;
19
19
  */
20
20
  export declare const getQstash: () => QstashClient | null;
21
21
  export declare const withRedis: <T>(fn: (redis: Redis) => Promise<T> | T) => Promise<T | null>;
22
- export declare const withQstash: <T>(fn: (qstash: QstashClient) => Promise<T> | T) => Promise<T | null>;
22
+ export declare const withQstash: <T>(fn: (qstash: QstashClient) => Promise<T> | T, operation?: string) => Promise<T | null>;
23
23
  //# sourceMappingURL=upstash-config.d.ts.map
@@ -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;AA2DzD,eAAO,MAAM,mBAAmB,GAAI,KAAK,MAAM,KAAG,MAAyD,CAAC;AAE5G,eAAO,MAAM,oBAAoB,GAAI,MAAM,MAAM,EAAE,KAAG,MAAM,EAAmC,CAAC;AAEhG,eAAO,MAAM,mBAAmB,QAAO,MAEtC,CAAC;AAEF,eAAO,MAAM,0BAA0B,GAAI,MAAM,MAAM,KAAG,MAMzD,CAAC;AAiMF;;;;;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"}
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;AAoHzD,eAAO,MAAM,mBAAmB,GAAI,KAAK,MAAM,KAAG,MAAyD,CAAC;AAE5G,eAAO,MAAM,oBAAoB,GAAI,MAAM,MAAM,EAAE,KAAG,MAAM,EAAmC,CAAC;AAEhG,eAAO,MAAM,mBAAmB,QAAO,MAEtC,CAAC;AAEF,eAAO,MAAM,0BAA0B,GAAI,MAAM,MAAM,KAAG,MAMzD,CAAC;AAyMF;;;;;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,EAC5C,kBAAqB,KACpB,OAAO,CAAC,CAAC,GAAG,IAAI,CAgBlB,CAAC"}
@@ -20,6 +20,41 @@ let qstashWarnedHealthSchedule = false;
20
20
  let redisHealthTimer = null;
21
21
  let qstashHealthTimer = null;
22
22
  let cachedRedisPrefixed = null;
23
+ const isUpstashDebugEnabled = () => process.env.UPSTASH_DEBUG === 'true';
24
+ const formatDuration = (startedAt) => `${Date.now() - startedAt}ms`;
25
+ const logUpstashDuration = (scope, operation, startedAt, status, error) => {
26
+ if (!isUpstashDebugEnabled()) {
27
+ return;
28
+ }
29
+ const message = `[Upstash Debug] ${scope} ${operation} completed in ${formatDuration(startedAt)} status=${status}`;
30
+ if (error == null) {
31
+ console.log(message);
32
+ return;
33
+ }
34
+ const errorMessage = error instanceof Error ? error.message : String(error);
35
+ console.log(`${message} error=${errorMessage}`);
36
+ };
37
+ const isPromiseLike = (value) => typeof value === 'object' && value !== null && typeof value.then === 'function';
38
+ const trackUpstashOperation = (scope, operation, startedAt, run) => {
39
+ try {
40
+ const result = run();
41
+ if (isPromiseLike(result)) {
42
+ return result.then((resolved) => {
43
+ logUpstashDuration(scope, operation, startedAt, 'ok');
44
+ return resolved;
45
+ }, (error) => {
46
+ logUpstashDuration(scope, operation, startedAt, 'error', error);
47
+ throw error;
48
+ });
49
+ }
50
+ logUpstashDuration(scope, operation, startedAt, 'ok');
51
+ return result;
52
+ }
53
+ catch (error) {
54
+ logUpstashDuration(scope, operation, startedAt, 'error', error);
55
+ throw error;
56
+ }
57
+ };
23
58
  const isNonEmpty = (value) => typeof value === 'string' && value.trim().length > 0;
24
59
  const isValidUrl = (value) => {
25
60
  try {
@@ -88,33 +123,39 @@ const createPrefixedPipeline = (target, prefix) => {
88
123
  return value;
89
124
  }
90
125
  return (...args) => {
126
+ const operation = typeof prop === 'string' ? prop : String(prop);
127
+ const startedAt = Date.now();
91
128
  if (prop === 'eval' || prop === 'evalsha' || prop === 'evalro' || prop === 'evalshaRo') {
92
129
  const [script, keys, argv] = args;
93
- return value.call(obj, script, prefixRedisKeys(prefix, keys), argv);
130
+ return trackUpstashOperation('Redis', operation, startedAt, () => value.call(obj, script, prefixRedisKeys(prefix, keys), argv));
94
131
  }
95
132
  if (prop === 'pipeline' || prop === 'multi') {
96
- const nested = value.call(obj);
97
- return createPrefixedPipeline(nested, prefix);
98
- }
99
- if (typeof prop === 'string' && keyArrayCommands.has(prop)) {
100
- const nextArgs = prefixVariadicKeyArgs(args, prefix);
101
- return value.apply(obj, nextArgs);
102
- }
103
- if (typeof prop === 'string' && allStringKeyCommands.has(prop)) {
104
- const nextArgs = prefixVariadicKeyArgs(args, prefix);
105
- return value.apply(obj, nextArgs);
133
+ return trackUpstashOperation('Redis', operation, startedAt, () => {
134
+ const nested = value.call(obj);
135
+ return createPrefixedPipeline(nested, prefix);
136
+ });
106
137
  }
107
- if (prop === 'mset') {
108
- const [entries] = args;
109
- const prefixedEntries = Object.fromEntries(Object.entries(entries).map(([key, entryValue]) => [prefixRedisKey(prefix, key), entryValue]));
110
- return value.call(obj, prefixedEntries);
111
- }
112
- if (prop === 'hmget') {
138
+ return trackUpstashOperation('Redis', operation, startedAt, () => {
139
+ if (typeof prop === 'string' && keyArrayCommands.has(prop)) {
140
+ const nextArgs = prefixVariadicKeyArgs(args, prefix);
141
+ return value.apply(obj, nextArgs);
142
+ }
143
+ if (typeof prop === 'string' && allStringKeyCommands.has(prop)) {
144
+ const nextArgs = prefixVariadicKeyArgs(args, prefix);
145
+ return value.apply(obj, nextArgs);
146
+ }
147
+ if (prop === 'mset') {
148
+ const [entries] = args;
149
+ const prefixedEntries = Object.fromEntries(Object.entries(entries).map(([key, entryValue]) => [prefixRedisKey(prefix, key), entryValue]));
150
+ return value.call(obj, prefixedEntries);
151
+ }
152
+ if (prop === 'hmget') {
153
+ const nextArgs = prefixFirstStringArg(args, prefix);
154
+ return value.apply(obj, nextArgs);
155
+ }
113
156
  const nextArgs = prefixFirstStringArg(args, prefix);
114
157
  return value.apply(obj, nextArgs);
115
- }
116
- const nextArgs = prefixFirstStringArg(args, prefix);
117
- return value.apply(obj, nextArgs);
158
+ });
118
159
  };
119
160
  },
120
161
  });
@@ -337,12 +378,22 @@ const withRedis = (fn) => tslib.__awaiter(void 0, void 0, void 0, function* () {
337
378
  }
338
379
  return fn(redis);
339
380
  });
340
- const withQstash = (fn) => tslib.__awaiter(void 0, void 0, void 0, function* () {
381
+ const withQstash = (fn_1, ...args_1) => tslib.__awaiter(void 0, [fn_1, ...args_1], void 0, function* (fn, operation = 'request') {
382
+ const startedAt = Date.now();
341
383
  const qstash = yield ensureQstash();
342
384
  if (!qstash) {
385
+ logUpstashDuration('QStash', operation, startedAt, 'unavailable');
343
386
  return null;
344
387
  }
345
- return fn(qstash);
388
+ try {
389
+ const result = yield fn(qstash);
390
+ logUpstashDuration('QStash', operation, startedAt, 'ok');
391
+ return result;
392
+ }
393
+ catch (error) {
394
+ logUpstashDuration('QStash', operation, startedAt, 'error', error);
395
+ throw error;
396
+ }
346
397
  });
347
398
 
348
399
  exports.getPrefixedQstashQueueName = getPrefixedQstashQueueName;
@@ -18,6 +18,41 @@ let qstashWarnedHealthSchedule = false;
18
18
  let redisHealthTimer = null;
19
19
  let qstashHealthTimer = null;
20
20
  let cachedRedisPrefixed = null;
21
+ const isUpstashDebugEnabled = () => process.env.UPSTASH_DEBUG === 'true';
22
+ const formatDuration = (startedAt) => `${Date.now() - startedAt}ms`;
23
+ const logUpstashDuration = (scope, operation, startedAt, status, error) => {
24
+ if (!isUpstashDebugEnabled()) {
25
+ return;
26
+ }
27
+ const message = `[Upstash Debug] ${scope} ${operation} completed in ${formatDuration(startedAt)} status=${status}`;
28
+ if (error == null) {
29
+ console.log(message);
30
+ return;
31
+ }
32
+ const errorMessage = error instanceof Error ? error.message : String(error);
33
+ console.log(`${message} error=${errorMessage}`);
34
+ };
35
+ const isPromiseLike = (value) => typeof value === 'object' && value !== null && typeof value.then === 'function';
36
+ const trackUpstashOperation = (scope, operation, startedAt, run) => {
37
+ try {
38
+ const result = run();
39
+ if (isPromiseLike(result)) {
40
+ return result.then((resolved) => {
41
+ logUpstashDuration(scope, operation, startedAt, 'ok');
42
+ return resolved;
43
+ }, (error) => {
44
+ logUpstashDuration(scope, operation, startedAt, 'error', error);
45
+ throw error;
46
+ });
47
+ }
48
+ logUpstashDuration(scope, operation, startedAt, 'ok');
49
+ return result;
50
+ }
51
+ catch (error) {
52
+ logUpstashDuration(scope, operation, startedAt, 'error', error);
53
+ throw error;
54
+ }
55
+ };
21
56
  const isNonEmpty = (value) => typeof value === 'string' && value.trim().length > 0;
22
57
  const isValidUrl = (value) => {
23
58
  try {
@@ -86,33 +121,39 @@ const createPrefixedPipeline = (target, prefix) => {
86
121
  return value;
87
122
  }
88
123
  return (...args) => {
124
+ const operation = typeof prop === 'string' ? prop : String(prop);
125
+ const startedAt = Date.now();
89
126
  if (prop === 'eval' || prop === 'evalsha' || prop === 'evalro' || prop === 'evalshaRo') {
90
127
  const [script, keys, argv] = args;
91
- return value.call(obj, script, prefixRedisKeys(prefix, keys), argv);
128
+ return trackUpstashOperation('Redis', operation, startedAt, () => value.call(obj, script, prefixRedisKeys(prefix, keys), argv));
92
129
  }
93
130
  if (prop === 'pipeline' || prop === 'multi') {
94
- const nested = value.call(obj);
95
- return createPrefixedPipeline(nested, prefix);
96
- }
97
- if (typeof prop === 'string' && keyArrayCommands.has(prop)) {
98
- const nextArgs = prefixVariadicKeyArgs(args, prefix);
99
- return value.apply(obj, nextArgs);
100
- }
101
- if (typeof prop === 'string' && allStringKeyCommands.has(prop)) {
102
- const nextArgs = prefixVariadicKeyArgs(args, prefix);
103
- return value.apply(obj, nextArgs);
131
+ return trackUpstashOperation('Redis', operation, startedAt, () => {
132
+ const nested = value.call(obj);
133
+ return createPrefixedPipeline(nested, prefix);
134
+ });
104
135
  }
105
- if (prop === 'mset') {
106
- const [entries] = args;
107
- const prefixedEntries = Object.fromEntries(Object.entries(entries).map(([key, entryValue]) => [prefixRedisKey(prefix, key), entryValue]));
108
- return value.call(obj, prefixedEntries);
109
- }
110
- if (prop === 'hmget') {
136
+ return trackUpstashOperation('Redis', operation, startedAt, () => {
137
+ if (typeof prop === 'string' && keyArrayCommands.has(prop)) {
138
+ const nextArgs = prefixVariadicKeyArgs(args, prefix);
139
+ return value.apply(obj, nextArgs);
140
+ }
141
+ if (typeof prop === 'string' && allStringKeyCommands.has(prop)) {
142
+ const nextArgs = prefixVariadicKeyArgs(args, prefix);
143
+ return value.apply(obj, nextArgs);
144
+ }
145
+ if (prop === 'mset') {
146
+ const [entries] = args;
147
+ const prefixedEntries = Object.fromEntries(Object.entries(entries).map(([key, entryValue]) => [prefixRedisKey(prefix, key), entryValue]));
148
+ return value.call(obj, prefixedEntries);
149
+ }
150
+ if (prop === 'hmget') {
151
+ const nextArgs = prefixFirstStringArg(args, prefix);
152
+ return value.apply(obj, nextArgs);
153
+ }
111
154
  const nextArgs = prefixFirstStringArg(args, prefix);
112
155
  return value.apply(obj, nextArgs);
113
- }
114
- const nextArgs = prefixFirstStringArg(args, prefix);
115
- return value.apply(obj, nextArgs);
156
+ });
116
157
  };
117
158
  },
118
159
  });
@@ -335,12 +376,22 @@ const withRedis = (fn) => __awaiter(void 0, void 0, void 0, function* () {
335
376
  }
336
377
  return fn(redis);
337
378
  });
338
- const withQstash = (fn) => __awaiter(void 0, void 0, void 0, function* () {
379
+ const withQstash = (fn_1, ...args_1) => __awaiter(void 0, [fn_1, ...args_1], void 0, function* (fn, operation = 'request') {
380
+ const startedAt = Date.now();
339
381
  const qstash = yield ensureQstash();
340
382
  if (!qstash) {
383
+ logUpstashDuration('QStash', operation, startedAt, 'unavailable');
341
384
  return null;
342
385
  }
343
- return fn(qstash);
386
+ try {
387
+ const result = yield fn(qstash);
388
+ logUpstashDuration('QStash', operation, startedAt, 'ok');
389
+ return result;
390
+ }
391
+ catch (error) {
392
+ logUpstashDuration('QStash', operation, startedAt, 'error', error);
393
+ throw error;
394
+ }
344
395
  });
345
396
 
346
397
  export { getPrefixedQstashQueueName, getPrefixedRedisKey, getPrefixedRedisKeys, getQstash, getQstashNamePrefix, getRedis, withQstash, withRedis };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windrun-huaiin/backend-core",
3
- "version": "29.0.2",
3
+ "version": "29.0.3",
4
4
  "description": "Shared backend primitives: Prisma schema/client, database services, routing helpers",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -149,8 +149,8 @@
149
149
  "tslib": "^2.8.1",
150
150
  "zod": "^4.3.6",
151
151
  "@windrun-huaiin/contracts": "^29.0.0",
152
- "@windrun-huaiin/third-ui": "^29.0.3",
153
- "@windrun-huaiin/lib": "^29.0.0"
152
+ "@windrun-huaiin/lib": "^29.0.0",
153
+ "@windrun-huaiin/third-ui": "^29.0.3"
154
154
  },
155
155
  "devDependencies": {
156
156
  "@rollup/plugin-alias": "^5.1.1",
@@ -99,7 +99,7 @@ export const publishMessage = async <TBody extends PublishBody>(
99
99
  messageId: typeof result === 'string' ? result : result?.messageId ?? null,
100
100
  message,
101
101
  };
102
- });
102
+ }, 'publishMessage');
103
103
  };
104
104
 
105
105
  /**
@@ -133,7 +133,7 @@ export const publishBroadcastMessage = async <TBody extends PublishBody>(
133
133
  messageIds,
134
134
  message,
135
135
  };
136
- });
136
+ }, 'publishBroadcastMessage');
137
137
  };
138
138
 
139
139
  /**
@@ -162,7 +162,7 @@ export const publishFIFOQueueMessage = async <TBody extends PublishBody>(
162
162
  messageId: typeof result === 'string' ? result : result?.messageId ?? null,
163
163
  message,
164
164
  };
165
- });
165
+ }, 'publishFIFOQueueMessage');
166
166
  };
167
167
 
168
168
  /**
@@ -183,7 +183,7 @@ export const publishDelayedMessage = async <TBody extends PublishBody>(
183
183
  messageId: typeof result === 'string' ? result : result?.messageId ?? null,
184
184
  message,
185
185
  };
186
- });
186
+ }, 'publishDelayedMessage');
187
187
  };
188
188
 
189
189
  export interface ScheduleMessageOptions<TBody extends PublishBody = PublishBody>
@@ -217,7 +217,7 @@ export const scheduleMessage = async <TBody extends PublishBody>(
217
217
  scheduleId: typeof result === 'string' ? result : result?.scheduleId ?? result?.id ?? null,
218
218
  message,
219
219
  };
220
- });
220
+ }, 'scheduleMessage');
221
221
  };
222
222
 
223
223
  /**
@@ -235,7 +235,7 @@ export const cancelSchedule = async (scheduleId: string): Promise<boolean> => {
235
235
  return true;
236
236
  }
237
237
  return false;
238
- });
238
+ }, 'cancelSchedule');
239
239
 
240
240
  return result ?? false;
241
241
  };
@@ -21,6 +21,63 @@ let redisHealthTimer: ReturnType<typeof setTimeout> | null = null;
21
21
  let qstashHealthTimer: ReturnType<typeof setTimeout> | null = null;
22
22
  let cachedRedisPrefixed: Redis | null = null;
23
23
 
24
+ const isUpstashDebugEnabled = (): boolean => process.env.UPSTASH_DEBUG === 'true';
25
+
26
+ const formatDuration = (startedAt: number): string => `${Date.now() - startedAt}ms`;
27
+
28
+ const logUpstashDuration = (
29
+ scope: string,
30
+ operation: string,
31
+ startedAt: number,
32
+ status: 'ok' | 'error' | 'unavailable',
33
+ error?: unknown
34
+ ): void => {
35
+ if (!isUpstashDebugEnabled()) {
36
+ return;
37
+ }
38
+
39
+ const message = `[Upstash Debug] ${scope} ${operation} completed in ${formatDuration(startedAt)} status=${status}`;
40
+ if (error == null) {
41
+ console.log(message);
42
+ return;
43
+ }
44
+
45
+ const errorMessage = error instanceof Error ? error.message : String(error);
46
+ console.log(`${message} error=${errorMessage}`);
47
+ };
48
+
49
+ const isPromiseLike = <T>(value: unknown): value is Promise<T> =>
50
+ typeof value === 'object' && value !== null && typeof (value as Promise<T>).then === 'function';
51
+
52
+ const trackUpstashOperation = <T>(
53
+ scope: string,
54
+ operation: string,
55
+ startedAt: number,
56
+ run: () => T
57
+ ): T => {
58
+ try {
59
+ const result = run();
60
+ if (isPromiseLike(result)) {
61
+ return result.then(
62
+ (resolved) => {
63
+ logUpstashDuration(scope, operation, startedAt, 'ok');
64
+ return resolved;
65
+ },
66
+ (error) => {
67
+ logUpstashDuration(scope, operation, startedAt, 'error', error);
68
+ throw error;
69
+ }
70
+ ) as T;
71
+ }
72
+
73
+ logUpstashDuration(scope, operation, startedAt, 'ok');
74
+ return result;
75
+ } catch (error) {
76
+ logUpstashDuration(scope, operation, startedAt, 'error', error);
77
+ throw error;
78
+ }
79
+ };
80
+
24
81
  const isNonEmpty = (value: string | undefined): value is string =>
25
82
  typeof value === 'string' && value.trim().length > 0;
26
83
 
@@ -115,46 +172,54 @@ const createPrefixedPipeline = <T extends object>(target: T, prefix: string): T
115
172
  }
116
173
 
117
174
  return (...args: unknown[]) => {
175
+ const operation = typeof prop === 'string' ? prop : String(prop);
176
+ const startedAt = Date.now();
118
177
  if (prop === 'eval' || prop === 'evalsha' || prop === 'evalro' || prop === 'evalshaRo') {
119
178
  const [script, keys, argv] = args as [string, string[], unknown[]];
120
- return (value as (...innerArgs: unknown[]) => unknown).call(
121
- obj,
122
- script,
123
- prefixRedisKeys(prefix, keys),
124
- argv
179
+ return trackUpstashOperation('Redis', operation, startedAt, () =>
180
+ (value as (...innerArgs: unknown[]) => unknown).call(
181
+ obj,
182
+ script,
183
+ prefixRedisKeys(prefix, keys),
184
+ argv
185
+ )
125
186
  );
126
187
  }
127
188
 
128
189
  if (prop === 'pipeline' || prop === 'multi') {
129
- const nested = (value as (...innerArgs: unknown[]) => unknown).call(obj);
130
- return createPrefixedPipeline(nested as T, prefix);
131
- }
132
-
133
- if (typeof prop === 'string' && keyArrayCommands.has(prop)) {
134
- const nextArgs = prefixVariadicKeyArgs(args, prefix);
135
- return (value as (...innerArgs: unknown[]) => unknown).apply(obj, nextArgs);
136
- }
137
-
138
- if (typeof prop === 'string' && allStringKeyCommands.has(prop)) {
139
- const nextArgs = prefixVariadicKeyArgs(args, prefix);
140
- return (value as (...innerArgs: unknown[]) => unknown).apply(obj, nextArgs);
190
+ return trackUpstashOperation('Redis', operation, startedAt, () => {
191
+ const nested = (value as (...innerArgs: unknown[]) => unknown).call(obj);
192
+ return createPrefixedPipeline(nested as T, prefix);
193
+ });
141
194
  }
142
195
 
143
- if (prop === 'mset') {
144
- const [entries] = args as [Record<string, unknown>];
145
- const prefixedEntries = Object.fromEntries(
146
- Object.entries(entries).map(([key, entryValue]) => [prefixRedisKey(prefix, key), entryValue])
147
- );
148
- return (value as (...innerArgs: unknown[]) => unknown).call(obj, prefixedEntries);
149
- }
196
+ return trackUpstashOperation('Redis', operation, startedAt, () => {
197
+ if (typeof prop === 'string' && keyArrayCommands.has(prop)) {
198
+ const nextArgs = prefixVariadicKeyArgs(args, prefix);
199
+ return (value as (...innerArgs: unknown[]) => unknown).apply(obj, nextArgs);
200
+ }
201
+
202
+ if (typeof prop === 'string' && allStringKeyCommands.has(prop)) {
203
+ const nextArgs = prefixVariadicKeyArgs(args, prefix);
204
+ return (value as (...innerArgs: unknown[]) => unknown).apply(obj, nextArgs);
205
+ }
206
+
207
+ if (prop === 'mset') {
208
+ const [entries] = args as [Record<string, unknown>];
209
+ const prefixedEntries = Object.fromEntries(
210
+ Object.entries(entries).map(([key, entryValue]) => [prefixRedisKey(prefix, key), entryValue])
211
+ );
212
+ return (value as (...innerArgs: unknown[]) => unknown).call(obj, prefixedEntries);
213
+ }
214
+
215
+ if (prop === 'hmget') {
216
+ const nextArgs = prefixFirstStringArg(args, prefix);
217
+ return (value as (...innerArgs: unknown[]) => unknown).apply(obj, nextArgs);
218
+ }
150
219
 
151
- if (prop === 'hmget') {
152
220
  const nextArgs = prefixFirstStringArg(args, prefix);
153
221
  return (value as (...innerArgs: unknown[]) => unknown).apply(obj, nextArgs);
154
- }
155
-
156
- const nextArgs = prefixFirstStringArg(args, prefix);
157
- return (value as (...innerArgs: unknown[]) => unknown).apply(obj, nextArgs);
222
+ });
158
223
  };
159
224
  },
160
225
  });
@@ -393,11 +458,22 @@ export const withRedis = async <T>(fn: (redis: Redis) => Promise<T> | T): Promis
393
458
  };
394
459
 
395
460
  export const withQstash = async <T>(
396
- fn: (qstash: QstashClient) => Promise<T> | T
461
+ fn: (qstash: QstashClient) => Promise<T> | T,
462
+ operation = 'request'
397
463
  ): Promise<T | null> => {
464
+ const startedAt = Date.now();
398
465
  const qstash = await ensureQstash();
399
466
  if (!qstash) {
467
+ logUpstashDuration('QStash', operation, startedAt, 'unavailable');
400
468
  return null;
401
469
  }
402
- return fn(qstash);
470
+
471
+ try {
472
+ const result = await fn(qstash);
473
+ logUpstashDuration('QStash', operation, startedAt, 'ok');
474
+ return result;
475
+ } catch (error) {
476
+ logUpstashDuration('QStash', operation, startedAt, 'error', error);
477
+ throw error;
478
+ }
403
479
  };