@sunnoy/wecom 1.1.2 → 1.3.0

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/utils.js CHANGED
@@ -1,251 +1,89 @@
1
1
  /**
2
- * Utility functions and helpers for WeCom plugin
2
+ * Utility helpers for the WeCom plugin.
3
3
  */
4
4
  export class TTLCache {
5
- options;
6
- cache = new Map();
7
- checkPeriod;
8
- cleanupTimer;
9
- constructor(options) {
10
- this.options = options;
11
- this.checkPeriod = options.checkPeriod || options.ttl;
12
- this.startCleanup();
13
- }
14
- set(key, value, ttl) {
15
- const expiresAt = Date.now() + (ttl || this.options.ttl);
16
- this.cache.set(key, { value, expiresAt });
17
- }
18
- get(key) {
19
- const entry = this.cache.get(key);
20
- if (!entry)
21
- return undefined;
22
- if (Date.now() > entry.expiresAt) {
23
- this.cache.delete(key);
24
- return undefined;
25
- }
26
- return entry.value;
27
- }
28
- has(key) {
29
- return this.get(key) !== undefined;
30
- }
31
- delete(key) {
32
- return this.cache.delete(key);
33
- }
34
- clear() {
35
- this.cache.clear();
36
- }
37
- size() {
38
- this.cleanup();
39
- return this.cache.size;
40
- }
41
- cleanup() {
42
- const now = Date.now();
43
- for (const [key, entry] of this.cache.entries()) {
44
- if (now > entry.expiresAt) {
45
- this.cache.delete(key);
46
- }
47
- }
48
- }
49
- startCleanup() {
50
- this.cleanupTimer = setInterval(() => {
51
- this.cleanup();
52
- }, this.checkPeriod);
53
- // Don't prevent process from exiting
54
- if (this.cleanupTimer.unref) {
55
- this.cleanupTimer.unref();
56
- }
57
- }
58
- destroy() {
59
- if (this.cleanupTimer) {
60
- clearInterval(this.cleanupTimer);
61
- }
62
- this.cache.clear();
63
- }
5
+ options;
6
+ cache = new Map();
7
+ checkPeriod;
8
+ cleanupTimer;
9
+ constructor(options) {
10
+ this.options = options;
11
+ this.checkPeriod = options.checkPeriod || options.ttl;
12
+ this.startCleanup();
13
+ }
14
+ set(key, value, ttl) {
15
+ const expiresAt = Date.now() + (ttl || this.options.ttl);
16
+ this.cache.set(key, { value, expiresAt });
17
+ }
18
+ get(key) {
19
+ const entry = this.cache.get(key);
20
+ if (!entry) {
21
+ return undefined;
22
+ }
23
+ if (Date.now() > entry.expiresAt) {
24
+ this.cache.delete(key);
25
+ return undefined;
26
+ }
27
+ return entry.value;
28
+ }
29
+ has(key) {
30
+ return this.get(key) !== undefined;
31
+ }
32
+ delete(key) {
33
+ return this.cache.delete(key);
34
+ }
35
+ clear() {
36
+ this.cache.clear();
37
+ }
38
+ size() {
39
+ this.cleanup();
40
+ return this.cache.size;
41
+ }
42
+ cleanup() {
43
+ const now = Date.now();
44
+ for (const [key, entry] of this.cache.entries()) {
45
+ if (now > entry.expiresAt) {
46
+ this.cache.delete(key);
47
+ }
48
+ }
49
+ }
50
+ startCleanup() {
51
+ this.cleanupTimer = setInterval(() => {
52
+ this.cleanup();
53
+ }, this.checkPeriod);
54
+ // Don't prevent process from exiting
55
+ if (this.cleanupTimer.unref) {
56
+ this.cleanupTimer.unref();
57
+ }
58
+ }
59
+ destroy() {
60
+ if (this.cleanupTimer) {
61
+ clearInterval(this.cleanupTimer);
62
+ }
63
+ this.cache.clear();
64
+ }
64
65
  }
65
66
  // ============================================================================
66
- // Promise Lock (for preventing race conditions)
67
- // ============================================================================
68
- export class PromiseLock {
69
- pending = new Map();
70
- async acquire(key, fn) {
71
- // If there's already a pending operation, wait for it
72
- const existing = this.pending.get(key);
73
- if (existing) {
74
- return existing;
75
- }
76
- // Start new operation
77
- const promise = fn();
78
- this.pending.set(key, promise);
79
- try {
80
- const result = await promise;
81
- return result;
82
- }
83
- finally {
84
- this.pending.delete(key);
85
- }
86
- }
87
- clear(key) {
88
- if (key) {
89
- this.pending.delete(key);
90
- }
91
- else {
92
- this.pending.clear();
93
- }
94
- }
95
- }
96
- export async function withRetry(fn, options = {}) {
97
- const { retries = 3, minTimeout = 1000, maxTimeout = 10000, factor = 2, randomize = true, onRetry, } = options;
98
- let lastError;
99
- for (let attempt = 0; attempt <= retries; attempt++) {
100
- try {
101
- return await fn();
102
- }
103
- catch (error) {
104
- lastError = error;
105
- if (attempt === retries) {
106
- throw lastError;
107
- }
108
- // Calculate backoff delay
109
- let delay = Math.min(minTimeout * Math.pow(factor, attempt), maxTimeout);
110
- if (randomize) {
111
- delay = delay * (0.5 + Math.random() * 0.5);
112
- }
113
- if (onRetry) {
114
- onRetry(lastError, attempt + 1);
115
- }
116
- await sleep(delay);
117
- }
118
- }
119
- throw lastError;
120
- }
121
- export function sleep(ms) {
122
- return new Promise((resolve) => setTimeout(resolve, ms));
123
- }
124
- // ============================================================================
125
- // Message Deduplication
67
+ // Message deduplication
126
68
  // ============================================================================
127
69
  export class MessageDeduplicator {
128
- seen = new TTLCache({ ttl: 300000 }); // 5 minutes
129
- isDuplicate(msgId) {
130
- if (this.seen.has(msgId)) {
131
- return true;
132
- }
133
- this.seen.set(msgId, true);
134
- return false;
135
- }
136
- markAsSeen(msgId) {
137
- this.seen.set(msgId, true);
138
- }
139
- }
140
- export function parseWecomError(errcode, errmsg) {
141
- // Reference: https://developer.work.weixin.qq.com/document/path/96213
142
- switch (errcode) {
143
- case -1:
144
- return {
145
- code: errcode,
146
- message: "System busy, retry later",
147
- retryable: true,
148
- category: "system",
149
- };
150
- case 0:
151
- return {
152
- code: errcode,
153
- message: "Success",
154
- retryable: false,
155
- category: "system",
156
- };
157
- case 40001:
158
- case 40014:
159
- case 42001:
160
- case 42007:
161
- case 42009:
162
- return {
163
- code: errcode,
164
- message: `Invalid or expired access_token: ${errmsg}`,
165
- retryable: true,
166
- category: "auth",
167
- };
168
- case 45009:
169
- return {
170
- code: errcode,
171
- message: "API rate limit exceeded",
172
- retryable: true,
173
- category: "rate_limit",
174
- };
175
- case 48002:
176
- return {
177
- code: errcode,
178
- message: "API concurrent call limit exceeded",
179
- retryable: true,
180
- category: "rate_limit",
181
- };
182
- case 40003:
183
- case 40013:
184
- case 40035:
185
- return {
186
- code: errcode,
187
- message: `Invalid parameter: ${errmsg}`,
188
- retryable: false,
189
- category: "invalid_input",
190
- };
191
- default:
192
- return {
193
- code: errcode,
194
- message: errmsg || "Unknown error",
195
- retryable: errcode >= 50000 && errcode < 60000, // System errors are retryable
196
- category: "unknown",
197
- };
198
- }
199
- }
200
- export function shouldRetryError(errcode) {
201
- const info = parseWecomError(errcode, "");
202
- return info.retryable;
203
- }
204
- // ============================================================================
205
- // URL Helpers
206
- // ============================================================================
207
- export function buildApiUrl(path, params) {
208
- const base = `https://qyapi.weixin.qq.com${path}`;
209
- if (!params || Object.keys(params).length === 0) {
210
- return base;
211
- }
212
- const query = new URLSearchParams(params).toString();
213
- return `${base}?${query}`;
214
- }
215
- // ============================================================================
216
- // Random String Generation
217
- // ============================================================================
218
- export function randomString(length) {
219
- const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
220
- let result = "";
221
- for (let i = 0; i < length; i++) {
222
- result += chars.charAt(Math.floor(Math.random() * chars.length));
223
- }
224
- return result;
70
+ seen = new TTLCache({ ttl: 300000 }); // 5 minutes
71
+ isDuplicate(msgId) {
72
+ if (this.seen.has(msgId)) {
73
+ return true;
74
+ }
75
+ this.seen.set(msgId, true);
76
+ return false;
77
+ }
78
+ markAsSeen(msgId) {
79
+ this.seen.set(msgId, true);
80
+ }
225
81
  }
226
82
  // ============================================================================
227
83
  // Constants
228
84
  // ============================================================================
229
85
  export const CONSTANTS = {
230
- // Token settings
231
- TOKEN_REFRESH_MARGIN_MS: 300000, // 5 minutes before expiry
232
- TOKEN_CACHE_KEY: "access_token",
233
- // Response URL settings
234
- RESPONSE_URL_TTL_MS: 3600000, // 1 hour
235
- RESPONSE_URL_MAX_USES: 1,
236
- // Media settings
237
- MEDIA_ID_TTL_MS: 259200000, // 3 days
238
- // Rate limiting
239
- MESSAGE_RATE_LIMIT_PER_MINUTE: 20,
240
- // Timeouts
241
- API_TIMEOUT_MS: 10000, // 10 seconds
242
- WEBHOOK_RESPONSE_TIMEOUT_MS: 5000, // 5 seconds
243
- // Retry settings
244
- DEFAULT_RETRY_COUNT: 3,
245
- DEFAULT_RETRY_MIN_DELAY_MS: 1000,
246
- DEFAULT_RETRY_MAX_DELAY_MS: 10000,
247
- // AES/Crypto
248
- AES_BLOCK_SIZE: 32,
249
- AES_KEY_LENGTH: 43,
86
+ // AES/Crypto
87
+ AES_BLOCK_SIZE: 32,
88
+ AES_KEY_LENGTH: 43,
250
89
  };
251
- //# sourceMappingURL=utils.js.map