@gpc-cli/api 1.0.20 → 1.0.22

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
@@ -21,8 +21,334 @@ var PlayApiError = class extends Error {
21
21
  };
22
22
 
23
23
  // src/http.ts
24
- import { readFile } from "fs/promises";
24
+ import { readFile, stat as stat2 } from "fs/promises";
25
25
  import { resolve, isAbsolute } from "path";
26
+
27
+ // src/resumable-upload.ts
28
+ import { open, stat } from "fs/promises";
29
+ var CHUNK_ALIGNMENT = 256 * 1024;
30
+ var DEFAULT_CHUNK_SIZE = 8 * 1024 * 1024;
31
+ var RESUMABLE_THRESHOLD = 5 * 1024 * 1024;
32
+ function envInt(name) {
33
+ const val = process.env[name];
34
+ if (val === void 0) return void 0;
35
+ const n = Number(val);
36
+ return Number.isFinite(n) ? n : void 0;
37
+ }
38
+ function resolveChunkSize(explicit) {
39
+ const size = explicit ?? envInt("GPC_UPLOAD_CHUNK_SIZE") ?? DEFAULT_CHUNK_SIZE;
40
+ if (size < CHUNK_ALIGNMENT || size % CHUNK_ALIGNMENT !== 0) {
41
+ throw new PlayApiError(
42
+ `Chunk size must be a multiple of 256 KB (got ${size} bytes)`,
43
+ "UPLOAD_INVALID_CHUNK_SIZE",
44
+ void 0,
45
+ `Use a multiple of 262144 (256 KB). Common values: 1048576 (1 MB), 8388608 (8 MB), 16777216 (16 MB).`
46
+ );
47
+ }
48
+ return size;
49
+ }
50
+ function jitteredDelay(base, attempt, max) {
51
+ const exponential = base * 2 ** attempt;
52
+ const capped = Math.min(exponential, max);
53
+ return capped * (0.5 + Math.random() * 0.5);
54
+ }
55
+ async function resumableUpload(uploadUrl, filePath, contentType, ctx, options) {
56
+ const chunkSize = resolveChunkSize(options?.chunkSize);
57
+ const maxResumeAttempts = options?.maxResumeAttempts ?? 5;
58
+ const onProgress = options?.onProgress;
59
+ const fileStats = await stat(filePath);
60
+ const totalBytes = fileStats.size;
61
+ let sessionUri = options?.resumeSessionUri;
62
+ if (!sessionUri) {
63
+ sessionUri = await initiateSession(uploadUrl, contentType, totalBytes, ctx);
64
+ }
65
+ const startTime = Date.now();
66
+ let offset = 0;
67
+ if (options?.resumeSessionUri) {
68
+ offset = await queryProgress(sessionUri, totalBytes, ctx);
69
+ }
70
+ let fh;
71
+ try {
72
+ fh = await open(filePath, "r");
73
+ const chunkBuffer = Buffer.alloc(chunkSize);
74
+ while (offset < totalBytes) {
75
+ const remaining = totalBytes - offset;
76
+ const bytesToRead = Math.min(chunkSize, remaining);
77
+ const { bytesRead } = await fh.read(chunkBuffer, 0, bytesToRead, offset);
78
+ if (bytesRead === 0) break;
79
+ const chunk = Buffer.from(chunkBuffer.buffer, chunkBuffer.byteOffset, bytesRead);
80
+ const rangeEnd = offset + bytesRead - 1;
81
+ const contentRange = `bytes ${offset}-${rangeEnd}/${totalBytes}`;
82
+ let result;
83
+ for (let attempt = 0; attempt <= maxResumeAttempts; attempt++) {
84
+ if (attempt > 0) {
85
+ const delay = jitteredDelay(1e3, attempt - 1, 3e4);
86
+ await new Promise((r) => setTimeout(r, delay));
87
+ try {
88
+ const serverOffset = await queryProgress(sessionUri, totalBytes, ctx);
89
+ if (serverOffset >= totalBytes) {
90
+ const completionResult = await fetchCompletionResponse(sessionUri, totalBytes, ctx);
91
+ if (completionResult) {
92
+ result = completionResult;
93
+ break;
94
+ }
95
+ result = { complete: true, response: { data: {}, status: 200 } };
96
+ break;
97
+ }
98
+ if (serverOffset >= offset + bytesRead) {
99
+ result = { complete: false };
100
+ break;
101
+ }
102
+ if (serverOffset > offset) {
103
+ }
104
+ } catch {
105
+ }
106
+ ctx.onRetry?.({
107
+ attempt,
108
+ method: "PUT",
109
+ path: sessionUri,
110
+ error: `Chunk upload failed at offset ${offset}, retrying`,
111
+ delayMs: Math.round(delay),
112
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
113
+ });
114
+ }
115
+ result = await sendChunk(sessionUri, chunk, contentRange, ctx);
116
+ if (result) break;
117
+ }
118
+ if (!result) {
119
+ throw new PlayApiError(
120
+ `Upload failed: chunk at offset ${offset} could not be sent after ${maxResumeAttempts + 1} attempts`,
121
+ "UPLOAD_CHUNK_FAILED",
122
+ void 0,
123
+ `The upload session is still valid for up to 1 week. Resume with: --resume-uri "${sessionUri}"`
124
+ );
125
+ }
126
+ offset += bytesRead;
127
+ if (onProgress) {
128
+ const elapsed = (Date.now() - startTime) / 1e3;
129
+ const bytesPerSecond = elapsed > 0 ? offset / elapsed : 0;
130
+ const remainingBytes = totalBytes - offset;
131
+ const etaSeconds = bytesPerSecond > 0 ? remainingBytes / bytesPerSecond : 0;
132
+ onProgress({
133
+ bytesUploaded: offset,
134
+ totalBytes,
135
+ percent: Math.round(offset / totalBytes * 100),
136
+ bytesPerSecond: Math.round(bytesPerSecond),
137
+ etaSeconds: Math.round(etaSeconds)
138
+ });
139
+ }
140
+ if (result.complete && result.response) {
141
+ return result.response;
142
+ }
143
+ }
144
+ try {
145
+ const serverOffset = await queryProgress(sessionUri, totalBytes, ctx);
146
+ if (serverOffset >= totalBytes) {
147
+ const completionResult = await fetchCompletionResponse(sessionUri, totalBytes, ctx);
148
+ if (completionResult?.response) {
149
+ return completionResult.response;
150
+ }
151
+ return { data: {}, status: 200 };
152
+ }
153
+ } catch {
154
+ }
155
+ throw new PlayApiError(
156
+ "Upload finished sending all bytes but did not receive a completion response",
157
+ "UPLOAD_NO_COMPLETION",
158
+ void 0,
159
+ `The upload session may still be valid. Resume with: --resume-uri "${sessionUri}"`
160
+ );
161
+ } finally {
162
+ await fh?.close();
163
+ }
164
+ }
165
+ async function initiateSession(uploadUrl, contentType, totalBytes, ctx) {
166
+ const token = await ctx.getAccessToken();
167
+ const url = uploadUrl.includes("?") ? `${uploadUrl}&uploadType=resumable` : `${uploadUrl}?uploadType=resumable`;
168
+ const controller = new AbortController();
169
+ const timer = setTimeout(() => controller.abort(), 6e4);
170
+ let response;
171
+ try {
172
+ response = await fetch(url, {
173
+ method: "POST",
174
+ headers: {
175
+ Authorization: `Bearer ${token}`,
176
+ "X-Upload-Content-Type": contentType,
177
+ "X-Upload-Content-Length": String(totalBytes),
178
+ "Content-Length": "0"
179
+ },
180
+ signal: controller.signal
181
+ });
182
+ } finally {
183
+ clearTimeout(timer);
184
+ }
185
+ if (!response.ok) {
186
+ const body = await response.text();
187
+ throw new PlayApiError(
188
+ `Failed to initiate resumable upload: ${response.status} ${body.slice(0, 200)}`,
189
+ "UPLOAD_INITIATE_FAILED",
190
+ response.status,
191
+ "Check that the package name, edit ID, and credentials are correct."
192
+ );
193
+ }
194
+ const location = response.headers.get("Location");
195
+ if (!location) {
196
+ throw new PlayApiError(
197
+ "Resumable upload initiation did not return a session URI (Location header missing)",
198
+ "UPLOAD_NO_SESSION_URI",
199
+ response.status,
200
+ "This is a Google API issue. Try again."
201
+ );
202
+ }
203
+ return location;
204
+ }
205
+ async function sendChunk(sessionUri, chunk, contentRange, ctx) {
206
+ const token = await ctx.getAccessToken();
207
+ const chunkTimeoutMs = 3e4 + Math.ceil(chunk.byteLength / (1024 * 1024)) * 1e3;
208
+ const controller = new AbortController();
209
+ const timer = setTimeout(() => controller.abort(), chunkTimeoutMs);
210
+ let response;
211
+ try {
212
+ response = await fetch(sessionUri, {
213
+ method: "PUT",
214
+ headers: {
215
+ Authorization: `Bearer ${token}`,
216
+ "Content-Length": String(chunk.byteLength),
217
+ "Content-Range": contentRange
218
+ },
219
+ body: chunk,
220
+ signal: controller.signal
221
+ });
222
+ } catch {
223
+ return void 0;
224
+ } finally {
225
+ clearTimeout(timer);
226
+ }
227
+ if (response.status === 200 || response.status === 201) {
228
+ const text = await response.text();
229
+ let data;
230
+ try {
231
+ data = text ? JSON.parse(text) : {};
232
+ } catch {
233
+ data = {};
234
+ }
235
+ return { complete: true, response: { data, status: response.status } };
236
+ }
237
+ if (response.status === 308) {
238
+ await response.body?.cancel();
239
+ return { complete: false };
240
+ }
241
+ if (response.status === 404) {
242
+ throw new PlayApiError(
243
+ "Upload session not found. The session may have expired.",
244
+ "UPLOAD_SESSION_NOT_FOUND",
245
+ 404,
246
+ "Start a new upload. Resumable upload sessions are valid for up to 1 week."
247
+ );
248
+ }
249
+ if (response.status === 410) {
250
+ throw new PlayApiError(
251
+ "Upload session has expired.",
252
+ "UPLOAD_SESSION_EXPIRED",
253
+ 410,
254
+ "Start a new upload from the beginning."
255
+ );
256
+ }
257
+ if (response.status === 401) {
258
+ await response.body?.cancel();
259
+ return void 0;
260
+ }
261
+ if (response.status === 429 || response.status >= 500) {
262
+ await response.body?.cancel();
263
+ return void 0;
264
+ }
265
+ const body = await response.text();
266
+ throw new PlayApiError(
267
+ `Upload chunk failed with status ${response.status}: ${body.slice(0, 200)}`,
268
+ `UPLOAD_HTTP_${response.status}`,
269
+ response.status,
270
+ "The upload encountered an unexpected error."
271
+ );
272
+ }
273
+ async function fetchCompletionResponse(sessionUri, totalBytes, ctx) {
274
+ const token = await ctx.getAccessToken();
275
+ const controller = new AbortController();
276
+ const timer = setTimeout(() => controller.abort(), 3e4);
277
+ try {
278
+ const response = await fetch(sessionUri, {
279
+ method: "PUT",
280
+ headers: {
281
+ Authorization: `Bearer ${token}`,
282
+ "Content-Length": "0",
283
+ "Content-Range": `bytes */${totalBytes}`
284
+ },
285
+ signal: controller.signal
286
+ });
287
+ if (response.status === 200 || response.status === 201) {
288
+ const text = await response.text();
289
+ let data;
290
+ try {
291
+ data = text ? JSON.parse(text) : {};
292
+ } catch {
293
+ data = {};
294
+ }
295
+ return { complete: true, response: { data, status: response.status } };
296
+ }
297
+ await response.body?.cancel();
298
+ return void 0;
299
+ } catch {
300
+ return void 0;
301
+ } finally {
302
+ clearTimeout(timer);
303
+ }
304
+ }
305
+ async function queryProgress(sessionUri, totalBytes, ctx) {
306
+ const token = await ctx.getAccessToken();
307
+ const controller = new AbortController();
308
+ const timer = setTimeout(() => controller.abort(), 3e4);
309
+ let response;
310
+ try {
311
+ response = await fetch(sessionUri, {
312
+ method: "PUT",
313
+ headers: {
314
+ Authorization: `Bearer ${token}`,
315
+ "Content-Length": "0",
316
+ "Content-Range": `bytes */${totalBytes}`
317
+ },
318
+ signal: controller.signal
319
+ });
320
+ } finally {
321
+ clearTimeout(timer);
322
+ }
323
+ if (response.status === 308) {
324
+ await response.body?.cancel();
325
+ const range = response.headers.get("Range");
326
+ if (range) {
327
+ const match = range.match(/bytes=0-(\d+)/);
328
+ if (match) {
329
+ return Number(match[1]) + 1;
330
+ }
331
+ }
332
+ return 0;
333
+ }
334
+ if (response.status === 200 || response.status === 201) {
335
+ await response.body?.cancel();
336
+ return totalBytes;
337
+ }
338
+ if (response.status === 404 || response.status === 410) {
339
+ await response.body?.cancel();
340
+ throw new PlayApiError(
341
+ "Upload session has expired while querying progress.",
342
+ "UPLOAD_SESSION_EXPIRED",
343
+ response.status,
344
+ "Start a new upload from the beginning."
345
+ );
346
+ }
347
+ await response.body?.cancel();
348
+ return 0;
349
+ }
350
+
351
+ // src/http.ts
26
352
  function stripHtml(text) {
27
353
  return text.replace(/<[^>]*>/g, " ").replace(/&[a-z]+;/gi, " ").replace(/\s+/g, " ").trim();
28
354
  }
@@ -60,14 +386,14 @@ function validateFilePath(filePath) {
60
386
  var BASE_URL = "https://androidpublisher.googleapis.com/androidpublisher/v3/applications";
61
387
  var UPLOAD_BASE_URL = "https://androidpublisher.googleapis.com/upload/androidpublisher/v3/applications";
62
388
  var INTERNAL_SHARING_UPLOAD_BASE_URL = "https://androidpublisher.googleapis.com/upload/internalappsharing/v3/applications";
63
- function envInt(name) {
389
+ function envInt2(name) {
64
390
  const val = process.env[name];
65
391
  if (val === void 0) return void 0;
66
392
  const n = Number(val);
67
393
  return Number.isFinite(n) ? n : void 0;
68
394
  }
69
395
  function resolveOption(explicit, envName, fallback) {
70
- return explicit ?? envInt(envName) ?? fallback;
396
+ return explicit ?? envInt2(envName) ?? fallback;
71
397
  }
72
398
  function mapStatusToError(status, body) {
73
399
  switch (status) {
@@ -120,17 +446,17 @@ function mapStatusToError(status, body) {
120
446
  }
121
447
  }
122
448
  function isRetryable(status) {
123
- return status === 429 || status >= 500;
449
+ return status === 408 || status === 429 || status >= 500;
124
450
  }
125
- function jitteredDelay(base, attempt, max) {
451
+ function jitteredDelay2(base, attempt, max) {
126
452
  const exponential = base * 2 ** attempt;
127
453
  const capped = Math.min(exponential, max);
128
454
  return capped * (0.5 + Math.random() * 0.5);
129
455
  }
130
456
  function createHttpClient(options) {
131
- const maxRetries = resolveOption(options.maxRetries, "GPC_MAX_RETRIES", 3);
457
+ const maxRetries = resolveOption(options.maxRetries, "GPC_MAX_RETRIES", 5);
132
458
  const timeout = resolveOption(options.timeout, "GPC_TIMEOUT", 3e4);
133
- const uploadTimeoutExplicit = options.uploadTimeout ?? envInt("GPC_UPLOAD_TIMEOUT");
459
+ const uploadTimeoutExplicit = options.uploadTimeout ?? envInt2("GPC_UPLOAD_TIMEOUT");
134
460
  const baseDelay = resolveOption(options.baseDelay, "GPC_BASE_DELAY", 1e3);
135
461
  const maxDelay = resolveOption(options.maxDelay, "GPC_MAX_DELAY", 6e4);
136
462
  const onRetry = options.onRetry;
@@ -144,7 +470,7 @@ function createHttpClient(options) {
144
470
  let lastError;
145
471
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
146
472
  if (attempt > 0) {
147
- const delay = jitteredDelay(baseDelay, attempt - 1, maxDelay);
473
+ const delay = jitteredDelay2(baseDelay, attempt - 1, maxDelay);
148
474
  await new Promise((r) => setTimeout(r, delay));
149
475
  }
150
476
  const controller = new AbortController();
@@ -181,7 +507,7 @@ function createHttpClient(options) {
181
507
  );
182
508
  if (isRetryable(response.status) && attempt < maxRetries) {
183
509
  lastError = err;
184
- const delay = jitteredDelay(baseDelay, attempt, maxDelay);
510
+ const delay = jitteredDelay2(baseDelay, attempt, maxDelay);
185
511
  onRetry?.({
186
512
  attempt: attempt + 1,
187
513
  method,
@@ -217,7 +543,7 @@ function createHttpClient(options) {
217
543
  method,
218
544
  path,
219
545
  error: timeoutErr.message,
220
- delayMs: Math.round(jitteredDelay(baseDelay, attempt, maxDelay)),
546
+ delayMs: Math.round(jitteredDelay2(baseDelay, attempt, maxDelay)),
221
547
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
222
548
  });
223
549
  continue;
@@ -237,7 +563,7 @@ function createHttpClient(options) {
237
563
  method,
238
564
  path,
239
565
  error: networkErr.message,
240
- delayMs: Math.round(jitteredDelay(baseDelay, attempt, maxDelay)),
566
+ delayMs: Math.round(jitteredDelay2(baseDelay, attempt, maxDelay)),
241
567
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
242
568
  });
243
569
  continue;
@@ -268,7 +594,7 @@ function createHttpClient(options) {
268
594
  let lastError;
269
595
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
270
596
  if (attempt > 0) {
271
- const delay = jitteredDelay(baseDelay, attempt - 1, maxDelay);
597
+ const delay = jitteredDelay2(baseDelay, attempt - 1, maxDelay);
272
598
  await new Promise((r) => setTimeout(r, delay));
273
599
  }
274
600
  const controller = new AbortController();
@@ -302,7 +628,7 @@ function createHttpClient(options) {
302
628
  );
303
629
  if (isRetryable(response.status) && attempt < maxRetries) {
304
630
  lastError = err;
305
- const delay = jitteredDelay(baseDelay, attempt, maxDelay);
631
+ const delay = jitteredDelay2(baseDelay, attempt, maxDelay);
306
632
  onRetry?.({
307
633
  attempt: attempt + 1,
308
634
  method: "POST",
@@ -339,7 +665,7 @@ function createHttpClient(options) {
339
665
  method: "POST",
340
666
  path: `upload ${path}`,
341
667
  error: timeoutErr.message,
342
- delayMs: Math.round(jitteredDelay(baseDelay, attempt, maxDelay)),
668
+ delayMs: Math.round(jitteredDelay2(baseDelay, attempt, maxDelay)),
343
669
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
344
670
  });
345
671
  continue;
@@ -359,7 +685,7 @@ function createHttpClient(options) {
359
685
  method: "POST",
360
686
  path: `upload ${path}`,
361
687
  error: networkErr.message,
362
- delayMs: Math.round(jitteredDelay(baseDelay, attempt, maxDelay)),
688
+ delayMs: Math.round(jitteredDelay2(baseDelay, attempt, maxDelay)),
363
689
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
364
690
  });
365
691
  continue;
@@ -395,6 +721,43 @@ function createHttpClient(options) {
395
721
  upload(path, filePath, contentType) {
396
722
  return uploadRequest(path, filePath, contentType);
397
723
  },
724
+ async uploadResumable(path, filePath, contentType, uploadOptions) {
725
+ const safeFilePath = validateFilePath(filePath);
726
+ const fileStats = await stat2(safeFilePath);
727
+ const threshold = envInt2("GPC_UPLOAD_RESUMABLE_THRESHOLD") ?? RESUMABLE_THRESHOLD;
728
+ if (fileStats.size < threshold && !uploadOptions?.resumeSessionUri) {
729
+ uploadOptions?.onProgress?.({
730
+ bytesUploaded: 0,
731
+ totalBytes: fileStats.size,
732
+ percent: 0,
733
+ bytesPerSecond: 0,
734
+ etaSeconds: 0
735
+ });
736
+ const result = await uploadRequest(path, safeFilePath, contentType);
737
+ uploadOptions?.onProgress?.({
738
+ bytesUploaded: fileStats.size,
739
+ totalBytes: fileStats.size,
740
+ percent: 100,
741
+ bytesPerSecond: 0,
742
+ etaSeconds: 0
743
+ });
744
+ return result;
745
+ }
746
+ const uploadUrl = `${UPLOAD_BASE_URL}${path}`;
747
+ return resumableUpload(
748
+ uploadUrl,
749
+ safeFilePath,
750
+ contentType,
751
+ {
752
+ getAccessToken: () => options.auth.getAccessToken(),
753
+ maxRetries,
754
+ baseDelay,
755
+ maxDelay,
756
+ onRetry
757
+ },
758
+ uploadOptions
759
+ );
760
+ },
398
761
  uploadInternal(path, filePath, contentType) {
399
762
  return uploadRequest(path, filePath, contentType, INTERNAL_SHARING_UPLOAD_BASE_URL);
400
763
  },
@@ -488,11 +851,12 @@ function createApiClient(options) {
488
851
  );
489
852
  return data.bundles;
490
853
  },
491
- async upload(packageName, editId, filePath) {
492
- const { data } = await http.upload(
854
+ async upload(packageName, editId, filePath, uploadOptions) {
855
+ const { data } = await http.uploadResumable(
493
856
  `/${packageName}/edits/${editId}/bundles`,
494
857
  filePath,
495
- "application/octet-stream"
858
+ "application/octet-stream",
859
+ uploadOptions
496
860
  );
497
861
  if (!data || !data.versionCode) {
498
862
  throw new PlayApiError(
@@ -769,7 +1133,6 @@ function createApiClient(options) {
769
1133
  async list(packageName, options2) {
770
1134
  const params = {};
771
1135
  if (options2?.token) params["token"] = options2.token;
772
- if (options2?.maxResults) params["maxResults"] = String(options2.maxResults);
773
1136
  const hasParams = Object.keys(params).length > 0;
774
1137
  const { data } = await http.get(
775
1138
  `/${packageName}/inappproducts`,
@@ -843,6 +1206,12 @@ function createApiClient(options) {
843
1206
  return data;
844
1207
  },
845
1208
  async getSubscriptionV1(packageName, subscriptionId, token) {
1209
+ if (typeof process !== "undefined" && process.emitWarning) {
1210
+ process.emitWarning(
1211
+ "purchases.subscriptions.get (v1) is deprecated by Google (shutdown Aug 2027). Use getSubscriptionV2() instead.",
1212
+ "DeprecationWarning"
1213
+ );
1214
+ }
846
1215
  const { data } = await http.get(
847
1216
  `/${packageName}/purchases/subscriptions/${subscriptionId}/tokens/${token}`
848
1217
  );
@@ -1138,12 +1507,78 @@ function createApiClient(options) {
1138
1507
  };
1139
1508
  }
1140
1509
 
1510
+ // src/rate-limiter.ts
1511
+ var RATE_LIMIT_BUCKETS = {
1512
+ default: { name: "default", maxTokens: 200, refillRate: 200, refillIntervalMs: 1e3 },
1513
+ reviewsGet: { name: "reviewsGet", maxTokens: 200, refillRate: 200, refillIntervalMs: 36e5 },
1514
+ reviewsPost: {
1515
+ name: "reviewsPost",
1516
+ maxTokens: 2e3,
1517
+ refillRate: 2e3,
1518
+ refillIntervalMs: 864e5
1519
+ },
1520
+ voidedBurst: { name: "voidedBurst", maxTokens: 30, refillRate: 30, refillIntervalMs: 3e4 },
1521
+ voidedDaily: {
1522
+ name: "voidedDaily",
1523
+ maxTokens: 6e3,
1524
+ refillRate: 6e3,
1525
+ refillIntervalMs: 864e5
1526
+ },
1527
+ reporting: { name: "reporting", maxTokens: 10, refillRate: 10, refillIntervalMs: 1e3 }
1528
+ };
1529
+ function createRateLimiter(buckets) {
1530
+ const states = /* @__PURE__ */ new Map();
1531
+ if (buckets) {
1532
+ for (const bucket of buckets) {
1533
+ states.set(bucket.name, {
1534
+ tokens: bucket.maxTokens,
1535
+ lastRefillTime: Date.now(),
1536
+ config: bucket
1537
+ });
1538
+ }
1539
+ }
1540
+ return {
1541
+ async acquire(bucket) {
1542
+ const state = states.get(bucket);
1543
+ if (!state) return;
1544
+ const now = Date.now();
1545
+ const elapsed = now - state.lastRefillTime;
1546
+ const refill = Math.floor(
1547
+ elapsed / state.config.refillIntervalMs * state.config.refillRate
1548
+ );
1549
+ if (refill > 0) {
1550
+ state.tokens = Math.min(state.config.maxTokens, state.tokens + refill);
1551
+ state.lastRefillTime = now;
1552
+ }
1553
+ if (state.tokens > 0) {
1554
+ state.tokens--;
1555
+ return;
1556
+ }
1557
+ const tokensNeeded = 1;
1558
+ const waitMs = Math.ceil(
1559
+ tokensNeeded / state.config.refillRate * state.config.refillIntervalMs
1560
+ );
1561
+ await new Promise((r) => setTimeout(r, waitMs));
1562
+ const afterWait = Date.now();
1563
+ const totalElapsed = afterWait - state.lastRefillTime;
1564
+ const newTokens = Math.floor(
1565
+ totalElapsed / state.config.refillIntervalMs * state.config.refillRate
1566
+ );
1567
+ state.tokens = Math.min(state.config.maxTokens, newTokens) - 1;
1568
+ state.lastRefillTime = afterWait;
1569
+ }
1570
+ };
1571
+ }
1572
+
1141
1573
  // src/reporting-client.ts
1142
1574
  var REPORTING_BASE_URL = "https://playdeveloperreporting.googleapis.com/v1beta1";
1143
1575
  function createReportingClient(options) {
1144
1576
  const http = createHttpClient({ ...options, baseUrl: REPORTING_BASE_URL });
1577
+ const reportingBucket = RATE_LIMIT_BUCKETS["reporting"];
1578
+ const limiter = options.rateLimiter ?? createRateLimiter(reportingBucket ? [reportingBucket] : []);
1145
1579
  return {
1146
1580
  async queryMetricSet(packageName, metricSet, query) {
1581
+ await limiter.acquire("reporting");
1147
1582
  const { data } = await http.post(
1148
1583
  `/apps/${packageName}/${metricSet}:query`,
1149
1584
  query
@@ -1151,10 +1586,12 @@ function createReportingClient(options) {
1151
1586
  return data;
1152
1587
  },
1153
1588
  async getAnomalies(packageName) {
1589
+ await limiter.acquire("reporting");
1154
1590
  const { data } = await http.get(`/apps/${packageName}/anomalies`);
1155
1591
  return data;
1156
1592
  },
1157
1593
  async searchErrorIssues(packageName, filter, pageSize, pageToken) {
1594
+ await limiter.acquire("reporting");
1158
1595
  const params = {};
1159
1596
  if (filter) params["filter"] = filter;
1160
1597
  if (pageSize) params["pageSize"] = String(pageSize);
@@ -1166,6 +1603,7 @@ function createReportingClient(options) {
1166
1603
  return data;
1167
1604
  },
1168
1605
  async searchErrorReports(packageName, issueName, pageSize, pageToken) {
1606
+ await limiter.acquire("reporting");
1169
1607
  const params = {};
1170
1608
  if (pageSize) params["pageSize"] = String(pageSize);
1171
1609
  if (pageToken) params["pageToken"] = pageToken;
@@ -1313,68 +1751,6 @@ function createEnterpriseClient(options) {
1313
1751
  };
1314
1752
  }
1315
1753
 
1316
- // src/rate-limiter.ts
1317
- var RATE_LIMIT_BUCKETS = {
1318
- default: { name: "default", maxTokens: 200, refillRate: 200, refillIntervalMs: 1e3 },
1319
- reviewsGet: { name: "reviewsGet", maxTokens: 200, refillRate: 200, refillIntervalMs: 36e5 },
1320
- reviewsPost: {
1321
- name: "reviewsPost",
1322
- maxTokens: 2e3,
1323
- refillRate: 2e3,
1324
- refillIntervalMs: 864e5
1325
- },
1326
- voidedBurst: { name: "voidedBurst", maxTokens: 30, refillRate: 30, refillIntervalMs: 3e4 },
1327
- voidedDaily: {
1328
- name: "voidedDaily",
1329
- maxTokens: 6e3,
1330
- refillRate: 6e3,
1331
- refillIntervalMs: 864e5
1332
- }
1333
- };
1334
- function createRateLimiter(buckets) {
1335
- const states = /* @__PURE__ */ new Map();
1336
- if (buckets) {
1337
- for (const bucket of buckets) {
1338
- states.set(bucket.name, {
1339
- tokens: bucket.maxTokens,
1340
- lastRefillTime: Date.now(),
1341
- config: bucket
1342
- });
1343
- }
1344
- }
1345
- return {
1346
- async acquire(bucket) {
1347
- const state = states.get(bucket);
1348
- if (!state) return;
1349
- const now = Date.now();
1350
- const elapsed = now - state.lastRefillTime;
1351
- const refill = Math.floor(
1352
- elapsed / state.config.refillIntervalMs * state.config.refillRate
1353
- );
1354
- if (refill > 0) {
1355
- state.tokens = Math.min(state.config.maxTokens, state.tokens + refill);
1356
- state.lastRefillTime = now;
1357
- }
1358
- if (state.tokens > 0) {
1359
- state.tokens--;
1360
- return;
1361
- }
1362
- const tokensNeeded = 1;
1363
- const waitMs = Math.ceil(
1364
- tokensNeeded / state.config.refillRate * state.config.refillIntervalMs
1365
- );
1366
- await new Promise((r) => setTimeout(r, waitMs));
1367
- const afterWait = Date.now();
1368
- const totalElapsed = afterWait - state.lastRefillTime;
1369
- const newTokens = Math.floor(
1370
- totalElapsed / state.config.refillIntervalMs * state.config.refillRate
1371
- );
1372
- state.tokens = Math.min(state.config.maxTokens, newTokens) - 1;
1373
- state.lastRefillTime = afterWait;
1374
- }
1375
- };
1376
- }
1377
-
1378
1754
  // src/paginate.ts
1379
1755
  async function* paginate(fetchPage, options) {
1380
1756
  let pageToken = options?.startPageToken;
@@ -1429,6 +1805,7 @@ async function paginateParallel(fetchPage, pageTokens, concurrency = 4) {
1429
1805
  export {
1430
1806
  PlayApiError,
1431
1807
  RATE_LIMIT_BUCKETS,
1808
+ RESUMABLE_THRESHOLD,
1432
1809
  createApiClient,
1433
1810
  createEnterpriseClient,
1434
1811
  createGamesClient,