dankgrinder 5.21.0 → 5.23.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.
@@ -194,7 +194,7 @@ async function enrichItems(items) {
194
194
  /**
195
195
  * Check inventory for all pages and return full item list.
196
196
  */
197
- async function runInventory({ channel, waitForDankMemer, client, accountId, redis }) {
197
+ async function runInventory({ channel, waitForDankMemer, client, accountId, redis, onPageProgress }) {
198
198
  LOG.cmd(`${c.white}${c.bold}pls inv${c.reset}`);
199
199
 
200
200
  await channel.send('pls inv');
@@ -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);
@@ -218,6 +225,9 @@ async function runInventory({ channel, waitForDankMemer, client, accountId, redi
218
225
  const allItems = [];
219
226
  let { page, total } = parsePageInfo(response);
220
227
  LOG.info(`[inv] Page ${page}/${total}`);
228
+ if (typeof onPageProgress === 'function') {
229
+ try { onPageProgress({ page, total }); } catch {}
230
+ }
221
231
  const visitedPages = new Set([page]);
222
232
 
223
233
  allItems.push(...parseInventoryPage(response));
@@ -316,6 +326,9 @@ async function runInventory({ channel, waitForDankMemer, client, accountId, redi
316
326
  visitedPages.add(page);
317
327
  pageChanged = true;
318
328
  LOG.info(`[inv] Page ${page}/${total}`);
329
+ if (typeof onPageProgress === 'function') {
330
+ try { onPageProgress({ page, total }); } catch {}
331
+ }
319
332
  break;
320
333
  }
321
334
  // Clear CV2 cache again for next retry
@@ -340,13 +353,19 @@ async function runInventory({ channel, waitForDankMemer, client, accountId, redi
340
353
  }
341
354
 
342
355
  const items = Object.values(itemMap);
343
- LOG.success(`[inv] Found ${items.length} unique items across ${visitedPages.size}/${total} pages`);
356
+ const pagesVisited = visitedPages.size;
357
+ const pagesTotal = Math.max(total || 1, 1);
358
+ const complete = page >= total || pagesVisited >= pagesTotal;
359
+ LOG.success(`[inv] Found ${items.length} unique items across ${pagesVisited}/${pagesTotal} pages`);
360
+ if (!complete) {
361
+ LOG.warn(`[inv] Incomplete pagination detected: stopped at page ${page}/${pagesTotal}`);
362
+ }
344
363
 
345
364
  const { totalValue, totalMarket } = await enrichItems(items);
346
365
  LOG.info(`[inv] Net value: ${c.bold}${c.green}⏣ ${totalValue.toLocaleString()}${c.reset} Market: ${c.bold}⏣ ${totalMarket.toLocaleString()}${c.reset}`);
347
366
 
348
367
  // Store in Redis — NO EXPIRY (permanent until next update)
349
- if (redis && accountId) {
368
+ if (complete && redis && accountId) {
350
369
  try {
351
370
  const payload = {
352
371
  items,
@@ -360,6 +379,8 @@ async function runInventory({ channel, waitForDankMemer, client, accountId, redi
360
379
  } catch (e) {
361
380
  LOG.error(`[inv] Redis store failed: ${e.message}`);
362
381
  }
382
+ } else if (!complete) {
383
+ LOG.warn('[inv] Skipping Redis store for incomplete inventory scan');
363
384
  }
364
385
 
365
386
  return {
@@ -368,6 +389,9 @@ async function runInventory({ channel, waitForDankMemer, client, accountId, redi
368
389
  totalValue,
369
390
  totalMarket,
370
391
  coins: 0,
392
+ complete,
393
+ pagesVisited,
394
+ pagesTotal,
371
395
  };
372
396
  }
373
397
 
package/lib/grinder.js CHANGED
@@ -1085,39 +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
- });
1107
- this.log('success', `Inventory: ${result.items?.length || 0} items, ⏣ ${(result.totalValue || 0).toLocaleString()} net`);
1108
- try {
1109
- await fetch(`${API_URL}/api/grinder/inventory`, {
1110
- method: 'POST',
1111
- headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
1112
- body: JSON.stringify({
1113
- account_id: this.account.id,
1114
- items: result.items || [],
1115
- totalValue: result.totalValue || 0,
1116
- }),
1117
- });
1118
- } 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 sleep(1500 + Math.floor(Math.random() * 1500));
1151
+ continue;
1152
+ }
1153
+ }
1154
+ }
1155
+
1156
+ throw lastErr || new Error('inventory check failed');
1119
1157
  } catch (e) {
1120
1158
  this.log('error', `Inventory check failed: ${e.message}`);
1159
+ return { ok: false, error: e.message };
1121
1160
  } finally {
1122
1161
  this._invRunning = false;
1123
1162
  this.busy = false;
@@ -2418,36 +2457,30 @@ async function start(apiKey, apiUrl) {
2418
2457
  log('info', `${c.dim}Checking inventory for all ${workers.length} accounts...${c.reset}`);
2419
2458
  let invDone = 0;
2420
2459
  let invFailed = 0;
2421
- const parsedInvConcurrency = Number.parseInt(String(process.env.INV_STARTUP_CONCURRENCY || ''), 10);
2422
- const invConcurrency = Math.max(1, Math.min(
2423
- workers.length,
2424
- Number.isFinite(parsedInvConcurrency) && parsedInvConcurrency > 0
2425
- ? parsedInvConcurrency
2426
- : Math.min(10, Math.max(3, Math.ceil(workers.length / 8)))
2427
- ));
2428
- log('info', `${c.dim}[inv-startup] parallel workers: ${invConcurrency}${c.reset}`);
2429
-
2430
- let invCursor = 0;
2431
- const runInventoryWorker = async (slot) => {
2432
- while (!shutdownCalled) {
2433
- const i = invCursor++;
2434
- if (i >= workers.length) break;
2435
- const w = workers[i];
2436
- const label = w?.username || w?.account?.label || w?.account?.id || `account-${i + 1}`;
2437
- log('info', `${c.dim}[inv-startup] ${i + 1}/${workers.length} ${label} ${c.dim}(runner ${slot})${c.reset}`);
2438
- try {
2439
- await w.checkInventory({ force: true, startupProgress: { current: i + 1, total: workers.length } });
2440
- invDone++;
2441
- } catch {
2442
- invFailed++;
2443
- }
2444
- const invComplete = invDone + invFailed;
2445
- log('info', `${c.dim}[inv-startup-progress] ${invComplete}/${workers.length} complete (${invDone} ok, ${invFailed} failed)${c.reset}`);
2446
- await new Promise(r => setTimeout(r, 200 + Math.floor(Math.random() * 400)));
2460
+ await Promise.all(workers.map(async (w, i) => {
2461
+ const label = w?.username || w?.account?.label || w?.account?.id || `account-${i + 1}`;
2462
+ log('info', `${c.dim}[inv-startup] ${i + 1}/${workers.length} ${label}${c.reset}`);
2463
+ try {
2464
+ const invRes = await w.checkInventory({
2465
+ force: true,
2466
+ startupProgress: { current: i + 1, total: workers.length },
2467
+ requireComplete: true,
2468
+ maxAttempts: 3,
2469
+ });
2470
+ if (invRes?.ok) invDone++;
2471
+ else invFailed++;
2472
+ } catch {
2473
+ invFailed++;
2447
2474
  }
2448
- };
2475
+ const invComplete = invDone + invFailed;
2476
+ log('info', `${c.dim}[inv-startup-progress] ${invComplete}/${workers.length} complete (${invDone} ok, ${invFailed} failed)${c.reset}`);
2477
+ }));
2478
+
2479
+ if (invFailed > 0) {
2480
+ log('error', `${c.red}Inventory phase incomplete: ${invDone}/${workers.length} complete, ${invFailed} failed/incomplete. Not starting grind loops.${c.reset}`);
2481
+ return;
2482
+ }
2449
2483
 
2450
- await Promise.all(Array.from({ length: invConcurrency }, (_, idx) => runInventoryWorker(idx + 1)));
2451
2484
  const invSummaryColor = invFailed > 0 ? c.yellow : c.green;
2452
2485
  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}`);
2453
2486
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dankgrinder",
3
- "version": "5.21.0",
3
+ "version": "5.23.0",
4
4
  "description": "Dank Memer automation engine — grind coins while you sleep",
5
5
  "bin": {
6
6
  "dankgrinder": "bin/dankgrinder.js"