@hienlh/ppm 0.8.63 → 0.8.64
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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.8.64] - 2026-03-27
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- **Usage polling dedup**: Concurrent `pollOnce` calls now share a single in-flight fetch instead of hammering the API
|
|
7
|
+
- **429 cooldown floor**: Minimum 60s cooldown on 429 responses, even if `retry-after` header is lower
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- **Browser preview tests**: Unit tests (12) for tunnel route validation and lifecycle; integration test (6) verifying real Cloudflare tunnel creation and access
|
|
11
|
+
|
|
3
12
|
## [0.8.63] - 2026-03-27
|
|
4
13
|
|
|
5
14
|
### Changed
|
package/package.json
CHANGED
|
@@ -21,8 +21,8 @@ interface ActiveTunnel {
|
|
|
21
21
|
startedAt: number;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
/** Active tunnels keyed by port */
|
|
25
|
-
const activeTunnels = new Map<number, ActiveTunnel>();
|
|
24
|
+
/** Active tunnels keyed by port — exported for testing */
|
|
25
|
+
export const activeTunnels = new Map<number, ActiveTunnel>();
|
|
26
26
|
|
|
27
27
|
/** Start a tunnel for a localhost port */
|
|
28
28
|
browserPreviewRoutes.post("/tunnel", async (c) => {
|
|
@@ -51,6 +51,10 @@ let pollTimer: ReturnType<typeof setInterval> | null = null;
|
|
|
51
51
|
|
|
52
52
|
// Per-token cooldown map: token prefix → earliest allowed fetch time
|
|
53
53
|
const tokenCooldowns = new Map<string, number>();
|
|
54
|
+
const MIN_COOLDOWN_MS = 60_000; // floor: at least 60s cooldown on 429
|
|
55
|
+
|
|
56
|
+
// Dedup: if a poll is already in-flight, reuse the same promise
|
|
57
|
+
let inflightPoll: Promise<void> | null = null;
|
|
54
58
|
|
|
55
59
|
// Legacy: Keychain token cache for users without accounts in DB
|
|
56
60
|
let tokenCache: { token: string; timestamp: number } | null = null;
|
|
@@ -113,9 +117,10 @@ async function fetchUsageForToken(token: string): Promise<ClaudeUsage> {
|
|
|
113
117
|
});
|
|
114
118
|
if (res.status === 429) {
|
|
115
119
|
const retryAfter = parseInt(res.headers.get("retry-after") ?? "60", 10);
|
|
120
|
+
const cooldownMs = Math.max(retryAfter * 1000, MIN_COOLDOWN_MS);
|
|
116
121
|
const cooldownKey = token.substring(0, 20);
|
|
117
|
-
tokenCooldowns.set(cooldownKey, Date.now() +
|
|
118
|
-
throw new Error(`Usage API 429 — cooldown ${
|
|
122
|
+
tokenCooldowns.set(cooldownKey, Date.now() + cooldownMs);
|
|
123
|
+
throw new Error(`Usage API 429 — cooldown ${Math.ceil(cooldownMs / 1000)}s`);
|
|
119
124
|
}
|
|
120
125
|
if (!res.ok) throw new Error(`Usage API returned ${res.status}`);
|
|
121
126
|
const raw = (await res.json()) as Record<string, any>;
|
|
@@ -228,7 +233,7 @@ async function fetchLegacySingleAccount(): Promise<void> {
|
|
|
228
233
|
} catch {}
|
|
229
234
|
}
|
|
230
235
|
|
|
231
|
-
async function
|
|
236
|
+
async function pollOnceInternal(): Promise<void> {
|
|
232
237
|
try {
|
|
233
238
|
const hasAccounts = accountService.list().length > 0;
|
|
234
239
|
if (hasAccounts) {
|
|
@@ -241,6 +246,13 @@ async function pollOnce(): Promise<void> {
|
|
|
241
246
|
}
|
|
242
247
|
}
|
|
243
248
|
|
|
249
|
+
/** Deduped: concurrent callers share a single in-flight fetch */
|
|
250
|
+
async function pollOnce(): Promise<void> {
|
|
251
|
+
if (inflightPoll) return inflightPoll;
|
|
252
|
+
inflightPoll = pollOnceInternal().finally(() => { inflightPoll = null; });
|
|
253
|
+
return inflightPoll;
|
|
254
|
+
}
|
|
255
|
+
|
|
244
256
|
// ---------------------------------------------------------------------------
|
|
245
257
|
// Public API
|
|
246
258
|
// ---------------------------------------------------------------------------
|