dankgrinder 5.22.0 → 5.24.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.
@@ -202,14 +202,21 @@ async function runInventory({ channel, waitForDankMemer, client, accountId, redi
202
202
 
203
203
  if (!response) {
204
204
  LOG.warn('[inv] No response');
205
- return { result: 'no response', items: [] };
205
+ return { result: 'no response', items: [], complete: false, pagesVisited: 0, pagesTotal: 0 };
206
206
  }
207
207
 
208
208
  if (isHoldTight(response)) {
209
209
  const reason = getHoldTightReason(response);
210
210
  LOG.warn(`[inv] Hold Tight${reason ? ` (reason: ${reason})` : ''}`);
211
211
  await sleep(30000);
212
- return { result: `hold tight (${reason || 'unknown'})`, items: [], holdTightReason: reason };
212
+ return {
213
+ result: `hold tight (${reason || 'unknown'})`,
214
+ items: [],
215
+ holdTightReason: reason,
216
+ complete: false,
217
+ pagesVisited: 0,
218
+ pagesTotal: 0,
219
+ };
213
220
  }
214
221
 
215
222
  if (isCV2(response)) await ensureCV2(response);
@@ -226,6 +233,7 @@ async function runInventory({ channel, waitForDankMemer, client, accountId, redi
226
233
  allItems.push(...parseInventoryPage(response));
227
234
 
228
235
  let guard = 0;
236
+ let noChangeCount = 0;
229
237
  while (page < total && guard < Math.max(20, total + 6)) {
230
238
  guard++;
231
239
  const buttons = getAllButtons(response);
@@ -305,12 +313,17 @@ async function runInventory({ channel, waitForDankMemer, client, accountId, redi
305
313
  delete response._cv2buttons;
306
314
 
307
315
  let pageChanged = false;
308
- for (let attempt = 0; attempt < 4; attempt++) {
309
- await sleep(attempt === 0 ? 600 : 1200);
316
+ for (let attempt = 0; attempt < 7; attempt++) {
317
+ await sleep(attempt === 0 ? 700 : 1100 + Math.min(attempt * 150, 600));
310
318
  try {
311
- const fresh = await channel.messages.fetch(response.id);
319
+ const fresh = await response.fetch(true);
312
320
  if (fresh) response = fresh;
313
- } catch {}
321
+ } catch {
322
+ try {
323
+ const fresh = await channel.messages.fetch(response.id);
324
+ if (fresh) response = fresh;
325
+ } catch {}
326
+ }
314
327
  if (isCV2(response)) await ensureCV2(response, true);
315
328
  const pageInfo = parsePageInfo(response);
316
329
  if (pageInfo.page > page && !visitedPages.has(pageInfo.page)) {
@@ -330,7 +343,16 @@ async function runInventory({ channel, waitForDankMemer, client, accountId, redi
330
343
  delete response._cv2buttons;
331
344
  }
332
345
 
333
- if (!pageChanged) break;
346
+ if (!pageChanged) {
347
+ noChangeCount++;
348
+ if (noChangeCount < 3) {
349
+ LOG.warn(`[inv] Page stuck at ${page}/${total}; retrying paginator click (${noChangeCount}/2)`);
350
+ await sleep(900 + Math.floor(Math.random() * 500));
351
+ continue;
352
+ }
353
+ break;
354
+ }
355
+ noChangeCount = 0;
334
356
  allItems.push(...parseInventoryPage(response));
335
357
  }
336
358
 
@@ -346,13 +368,19 @@ async function runInventory({ channel, waitForDankMemer, client, accountId, redi
346
368
  }
347
369
 
348
370
  const items = Object.values(itemMap);
349
- LOG.success(`[inv] Found ${items.length} unique items across ${visitedPages.size}/${total} pages`);
371
+ const pagesVisited = visitedPages.size;
372
+ const pagesTotal = Math.max(total || 1, 1);
373
+ const complete = page >= total || pagesVisited >= pagesTotal;
374
+ LOG.success(`[inv] Found ${items.length} unique items across ${pagesVisited}/${pagesTotal} pages`);
375
+ if (!complete) {
376
+ LOG.warn(`[inv] Incomplete pagination detected: stopped at page ${page}/${pagesTotal}`);
377
+ }
350
378
 
351
379
  const { totalValue, totalMarket } = await enrichItems(items);
352
380
  LOG.info(`[inv] Net value: ${c.bold}${c.green}⏣ ${totalValue.toLocaleString()}${c.reset} Market: ${c.bold}⏣ ${totalMarket.toLocaleString()}${c.reset}`);
353
381
 
354
382
  // Store in Redis — NO EXPIRY (permanent until next update)
355
- if (redis && accountId) {
383
+ if (complete && redis && accountId) {
356
384
  try {
357
385
  const payload = {
358
386
  items,
@@ -366,6 +394,8 @@ async function runInventory({ channel, waitForDankMemer, client, accountId, redi
366
394
  } catch (e) {
367
395
  LOG.error(`[inv] Redis store failed: ${e.message}`);
368
396
  }
397
+ } else if (!complete) {
398
+ LOG.warn('[inv] Skipping Redis store for incomplete inventory scan');
369
399
  }
370
400
 
371
401
  return {
@@ -374,6 +404,9 @@ async function runInventory({ channel, waitForDankMemer, client, accountId, redi
374
404
  totalValue,
375
405
  totalMarket,
376
406
  coins: 0,
407
+ complete,
408
+ pagesVisited,
409
+ pagesTotal,
377
410
  };
378
411
  }
379
412
 
package/lib/grinder.js CHANGED
@@ -1085,42 +1085,78 @@ class AccountWorker {
1085
1085
 
1086
1086
  // ── Check Balance ───────────────────────────────────────────
1087
1087
  async checkInventory(options = {}) {
1088
- const { force = false, startupProgress = null } = options;
1089
- if (this._invRunning) return;
1090
- if (!force && this._lastInvCheck && Date.now() - this._lastInvCheck < 300_000) return;
1088
+ const {
1089
+ force = false,
1090
+ startupProgress = null,
1091
+ requireComplete = false,
1092
+ maxAttempts = 1,
1093
+ } = options;
1094
+ if (this._invRunning) return { ok: false, skipped: 'busy' };
1095
+ if (!force && this._lastInvCheck && Date.now() - this._lastInvCheck < 300_000) return { ok: false, skipped: 'recent' };
1091
1096
  this._invRunning = true;
1092
1097
  this._lastInvCheck = Date.now();
1093
1098
  this.busy = true;
1094
1099
  try {
1095
- if (startupProgress && Number.isInteger(startupProgress.current) && Number.isInteger(startupProgress.total)) {
1096
- this.log('info', `Checking inventory... (${startupProgress.current}/${startupProgress.total})`);
1097
- } else {
1098
- this.log('info', 'Checking inventory...');
1099
- }
1100
- const result = await commands.runInventory({
1101
- channel: this.channel,
1102
- waitForDankMemer: (timeout) => this.waitForDankMemer(timeout),
1103
- client: this.client,
1104
- accountId: this.account.id,
1105
- redis,
1106
- onPageProgress: ({ page, total }) => {
1107
- this.log('info', `Inventory pages: ${page}/${total}`);
1108
- },
1109
- });
1110
- this.log('success', `Inventory: ${result.items?.length || 0} items, ⏣ ${(result.totalValue || 0).toLocaleString()} net`);
1111
- try {
1112
- await fetch(`${API_URL}/api/grinder/inventory`, {
1113
- method: 'POST',
1114
- headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
1115
- body: JSON.stringify({
1116
- account_id: this.account.id,
1117
- items: result.items || [],
1118
- totalValue: result.totalValue || 0,
1119
- }),
1120
- });
1121
- } catch {}
1100
+ const tries = Math.max(1, Number.isFinite(maxAttempts) ? maxAttempts : 1);
1101
+ let lastErr = null;
1102
+ for (let attempt = 1; attempt <= tries; attempt++) {
1103
+ if (startupProgress && Number.isInteger(startupProgress.current) && Number.isInteger(startupProgress.total)) {
1104
+ this.log('info', `Checking inventory... (${startupProgress.current}/${startupProgress.total}) [try ${attempt}/${tries}]`);
1105
+ } else {
1106
+ this.log('info', `Checking inventory...${tries > 1 ? ` [try ${attempt}/${tries}]` : ''}`);
1107
+ }
1108
+
1109
+ try {
1110
+ const result = await commands.runInventory({
1111
+ channel: this.channel,
1112
+ waitForDankMemer: (timeout) => this.waitForDankMemer(timeout),
1113
+ client: this.client,
1114
+ accountId: this.account.id,
1115
+ redis,
1116
+ onPageProgress: ({ page, total }) => {
1117
+ this.log('info', `Inventory pages: ${page}/${total}`);
1118
+ },
1119
+ });
1120
+
1121
+ if (requireComplete && !result.complete) {
1122
+ throw new Error(`incomplete pages (${result.pagesVisited || 0}/${result.pagesTotal || 0})`);
1123
+ }
1124
+
1125
+ this.log('success', `Inventory: ${result.items?.length || 0} items, ⏣ ${(result.totalValue || 0).toLocaleString()} net`);
1126
+ try {
1127
+ await fetch(`${API_URL}/api/grinder/inventory`, {
1128
+ method: 'POST',
1129
+ headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
1130
+ body: JSON.stringify({
1131
+ account_id: this.account.id,
1132
+ items: result.items || [],
1133
+ totalValue: result.totalValue || 0,
1134
+ }),
1135
+ });
1136
+ } catch {}
1137
+
1138
+ return {
1139
+ ok: true,
1140
+ complete: !!result.complete,
1141
+ pagesVisited: result.pagesVisited || 0,
1142
+ pagesTotal: result.pagesTotal || 0,
1143
+ attempts: attempt,
1144
+ result,
1145
+ };
1146
+ } catch (e) {
1147
+ lastErr = e;
1148
+ if (attempt < tries) {
1149
+ this.log('warn', `Inventory attempt ${attempt}/${tries} failed (${e.message}). Retrying...`);
1150
+ await new Promise((r) => setTimeout(r, 1500 + Math.floor(Math.random() * 1500)));
1151
+ continue;
1152
+ }
1153
+ }
1154
+ }
1155
+
1156
+ throw lastErr || new Error('inventory check failed');
1122
1157
  } catch (e) {
1123
1158
  this.log('error', `Inventory check failed: ${e.message}`);
1159
+ return { ok: false, error: e.message };
1124
1160
  } finally {
1125
1161
  this._invRunning = false;
1126
1162
  this.busy = false;
@@ -2402,17 +2438,31 @@ async function start(apiKey, apiUrl) {
2402
2438
  console.log('');
2403
2439
 
2404
2440
  // Phase 1: Login all accounts (staggered to avoid 429s)
2405
- const BATCH_SIZE = 5;
2406
- const BATCH_DELAY_MS = 3000;
2441
+ const LOGIN_PROGRESS_EVERY = 5;
2442
+ const parsedGapMin = Number.parseInt(String(process.env.LOGIN_GAP_MIN_MS || '100'), 10);
2443
+ const parsedGapMax = Number.parseInt(String(process.env.LOGIN_GAP_MAX_MS || '300'), 10);
2444
+ const LOGIN_GAP_MIN_MS = Number.isFinite(parsedGapMin) && parsedGapMin >= 0 ? parsedGapMin : 100;
2445
+ const LOGIN_GAP_MAX_MS = Number.isFinite(parsedGapMax) && parsedGapMax >= LOGIN_GAP_MIN_MS ? parsedGapMax : Math.max(LOGIN_GAP_MIN_MS, 300);
2446
+
2447
+ const randomLoginGap = () => {
2448
+ if (LOGIN_GAP_MAX_MS <= LOGIN_GAP_MIN_MS) return LOGIN_GAP_MIN_MS;
2449
+ return LOGIN_GAP_MIN_MS + Math.floor(Math.random() * (LOGIN_GAP_MAX_MS - LOGIN_GAP_MIN_MS + 1));
2450
+ };
2451
+
2407
2452
  for (let i = 0; i < accounts.length; i++) {
2408
2453
  if (shutdownCalled) break;
2409
2454
  const worker = new AccountWorker(accounts[i], i);
2410
2455
  workers.push(worker);
2411
2456
  workerMap.set(accounts[i].id, worker);
2412
2457
  await worker.start();
2413
- if ((i + 1) % BATCH_SIZE === 0 && i + 1 < accounts.length) {
2414
- log('info', `${c.dim}Logged in ${i + 1}/${accounts.length}, next batch in ${BATCH_DELAY_MS / 1000}s...${c.reset}`);
2415
- await new Promise(r => setTimeout(r, BATCH_DELAY_MS));
2458
+ if (i + 1 < accounts.length) {
2459
+ const gapMs = randomLoginGap();
2460
+ if ((i + 1) % LOGIN_PROGRESS_EVERY === 0) {
2461
+ log('info', `${c.dim}Logged in ${i + 1}/${accounts.length}, next account in ${gapMs}ms...${c.reset}`);
2462
+ }
2463
+ await new Promise(r => setTimeout(r, gapMs));
2464
+ }
2465
+ if ((i + 1) % LOGIN_PROGRESS_EVERY === 0) {
2416
2466
  hintGC();
2417
2467
  }
2418
2468
  }
@@ -2425,14 +2475,26 @@ async function start(apiKey, apiUrl) {
2425
2475
  const label = w?.username || w?.account?.label || w?.account?.id || `account-${i + 1}`;
2426
2476
  log('info', `${c.dim}[inv-startup] ${i + 1}/${workers.length} ${label}${c.reset}`);
2427
2477
  try {
2428
- await w.checkInventory({ force: true, startupProgress: { current: i + 1, total: workers.length } });
2429
- invDone++;
2478
+ const invRes = await w.checkInventory({
2479
+ force: true,
2480
+ startupProgress: { current: i + 1, total: workers.length },
2481
+ requireComplete: true,
2482
+ maxAttempts: 3,
2483
+ });
2484
+ if (invRes?.ok) invDone++;
2485
+ else invFailed++;
2430
2486
  } catch {
2431
2487
  invFailed++;
2432
2488
  }
2433
2489
  const invComplete = invDone + invFailed;
2434
2490
  log('info', `${c.dim}[inv-startup-progress] ${invComplete}/${workers.length} complete (${invDone} ok, ${invFailed} failed)${c.reset}`);
2435
2491
  }));
2492
+
2493
+ if (invFailed > 0) {
2494
+ log('error', `${c.red}Inventory phase incomplete: ${invDone}/${workers.length} complete, ${invFailed} failed/incomplete. Not starting grind loops.${c.reset}`);
2495
+ return;
2496
+ }
2497
+
2436
2498
  const invSummaryColor = invFailed > 0 ? c.yellow : c.green;
2437
2499
  log('success', `${c.dim}Inventory phase complete: ${invSummaryColor}${invDone}/${workers.length}${c.reset}${c.dim} done${invFailed > 0 ? `, ${c.yellow}${invFailed} failed${c.reset}${c.dim}` : ''}. Starting grind loops...${c.reset}`);
2438
2500
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dankgrinder",
3
- "version": "5.22.0",
3
+ "version": "5.24.0",
4
4
  "description": "Dank Memer automation engine — grind coins while you sleep",
5
5
  "bin": {
6
6
  "dankgrinder": "bin/dankgrinder.js"