scrapely 1.2.9 → 1.2.10

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.
@@ -122,6 +122,8 @@ export declare class ChargingManager {
122
122
  private getEventPricing;
123
123
  private normalizeChargingEventName;
124
124
  private calculateMaxChargesByPrice;
125
+ /** @internal Remaining allowance per event (pushDataAndCharge's uncertain path). */
126
+ _chargeableWithinLimit(): Record<string, number>;
125
127
  private getChargeableWithinLimit;
126
128
  private quarantineStalePending;
127
129
  /**
@@ -137,7 +139,14 @@ export declare class ChargingManager {
137
139
  * (POST /v2/actor-runs/:runId/charge/void), then unwind the local
138
140
  * reservation. Returns true when the platform voided it.
139
141
  */
140
- _voidCharge(eventName: string, operationId: string, reservedCount: number): Promise<boolean>;
142
+ /**
143
+ * @internal Cancel a charge whose outcome was never confirmed, for items that
144
+ * were then not stored: drop the pending intent (so the exit flush never
145
+ * posts it) and void it in case the original request did apply. A 404 here
146
+ * means it never applied.
147
+ */
148
+ _cancelUncertainCharge(eventName: string, operationId: string, reservedCount: number): Promise<boolean>;
149
+ _voidCharge(eventName: string, operationId: string, reservedCount: number, notFoundMeansVoided?: boolean): Promise<boolean>;
141
150
  /**
142
151
  * @internal Retry unconfirmed voids (caller holds the operation lock).
143
152
  * Returns false while any earlier void is still unconfirmed.
package/dist/charging.js CHANGED
@@ -345,6 +345,10 @@ class ChargingManager {
345
345
  const roundedResult = Math.floor(Number(unroundedResult.toFixed(4)));
346
346
  return Math.max(0, roundedResult);
347
347
  }
348
+ /** @internal Remaining allowance per event (pushDataAndCharge's uncertain path). */
349
+ _chargeableWithinLimit() {
350
+ return this.chargingState === undefined ? {} : this.getChargeableWithinLimit();
351
+ }
348
352
  getChargeableWithinLimit() {
349
353
  if (Object.keys(this.pricingInfo).length === 0) {
350
354
  return {};
@@ -414,7 +418,17 @@ class ChargingManager {
414
418
  * (POST /v2/actor-runs/:runId/charge/void), then unwind the local
415
419
  * reservation. Returns true when the platform voided it.
416
420
  */
417
- async _voidCharge(eventName, operationId, reservedCount) {
421
+ /**
422
+ * @internal Cancel a charge whose outcome was never confirmed, for items that
423
+ * were then not stored: drop the pending intent (so the exit flush never
424
+ * posts it) and void it in case the original request did apply. A 404 here
425
+ * means it never applied.
426
+ */
427
+ async _cancelUncertainCharge(eventName, operationId, reservedCount) {
428
+ this.pendingCharges.delete(operationId);
429
+ return this._voidCharge(eventName, operationId, reservedCount, true);
430
+ }
431
+ async _voidCharge(eventName, operationId, reservedCount, notFoundMeansVoided = false) {
418
432
  if (!this.isAtHome || !this.actorRunId)
419
433
  return false;
420
434
  const runClient = this.apifyClient.run(this.actorRunId);
@@ -431,6 +445,13 @@ class ChargingManager {
431
445
  }
432
446
  catch (err) {
433
447
  const status = httpStatusOf(err);
448
+ // Only the void route's own answer proves it never applied; a server
449
+ // without the route (older deployment) also answers 404.
450
+ if (status === 404 && notFoundMeansVoided && apiErrorTypeOf(err) === 'charge-not-found') {
451
+ // The unconfirmed charge never reached the ledger: nothing billed.
452
+ this.applyAcceptedCount(eventName, reservedCount, 0);
453
+ return true;
454
+ }
434
455
  // 404: nothing to void; 409: already settled; 4xx auth: permanent.
435
456
  // The charge stands; keep the local reservation that matches it.
436
457
  if (status !== undefined && status >= 400 && status < 500)
@@ -440,7 +461,7 @@ class ChargingManager {
440
461
  }
441
462
  }
442
463
  // Outcome unknown: keep the reservation and retry before the next push / at exit.
443
- this.pendingVoids.push({ eventName, operationId, reservedCount });
464
+ this.pendingVoids.push({ eventName, operationId, reservedCount, notFoundMeansVoided });
444
465
  console.error(`[Scrapely] Void of charge ${operationId} ('${eventName}') not confirmed yet; will retry`);
445
466
  return false;
446
467
  }
@@ -452,7 +473,7 @@ class ChargingManager {
452
473
  const pending = this.pendingVoids;
453
474
  this.pendingVoids = [];
454
475
  for (const v of pending) {
455
- await this._voidCharge(v.eventName, v.operationId, v.reservedCount);
476
+ await this._voidCharge(v.eventName, v.operationId, v.reservedCount, v.notFoundMeansVoided);
456
477
  }
457
478
  return this.pendingVoids.length === 0;
458
479
  }
@@ -784,10 +805,30 @@ async function pushDataAndCharge({ chargingManager, items, eventName, isDefaultD
784
805
  try {
785
806
  for (const [name, chargeCount] of ordered) {
786
807
  chargingManager._takeLastAcceptedOperationId?.();
787
- const r = await chargingManager._chargeExclusive({
788
- eventName: name,
789
- count: chargeCount,
790
- });
808
+ let r;
809
+ try {
810
+ r = await chargingManager._chargeExclusive({
811
+ eventName: name,
812
+ count: chargeCount,
813
+ });
814
+ }
815
+ catch (chargeErr) {
816
+ if (!(chargeErr instanceof ChargeUncertainError))
817
+ throw chargeErr;
818
+ // The platform did not confirm the charge (5xx / timeouts after
819
+ // retries). Never drop the items for that: the intent stays
820
+ // pending with its idempotency key and is re-posted by the exit
821
+ // flush, so the items are stored now and billed once.
822
+ console.warn(`[Scrapely] Charge for '${name}' not confirmed yet (${chargeErr.operationId}); storing the items and retrying the charge later`);
823
+ r = {
824
+ eventChargeLimitReached: false,
825
+ chargedCount: chargeErr.count,
826
+ chargeableWithinLimit: chargingManager._chargeableWithinLimit?.() ?? {},
827
+ };
828
+ results.push(r);
829
+ reservedInOp.push({ name, reserved: chargeErr.count, operationId: chargeErr.operationId, uncertain: true });
830
+ continue;
831
+ }
791
832
  results.push(r);
792
833
  reservedInOp.push({ name, reserved: r.chargedCount, operationId: chargingManager._takeLastAcceptedOperationId?.() });
793
834
  }
@@ -814,7 +855,12 @@ async function pushDataAndCharge({ chargingManager, items, eventName, isDefaultD
814
855
  const [name] = ordered[i];
815
856
  const reserved = results[i].chargedCount;
816
857
  if (reserved > acceptedForItems) {
817
- chargingManager.applyAcceptedCount(name, reserved, acceptedForItems);
858
+ // Nothing stored and a platform charge was made: leave its local
859
+ // count to the void below, which releases it only once the
860
+ // platform confirms (never on a server without the void route).
861
+ const platformOp = acceptedForItems <= 0 && reservedInOp[i]?.operationId;
862
+ if (!platformOp)
863
+ chargingManager.applyAcceptedCount(name, reserved, acceptedForItems);
818
864
  results[i] = {
819
865
  ...results[i],
820
866
  chargedCount: acceptedForItems,
@@ -824,6 +870,20 @@ async function pushDataAndCharge({ chargingManager, items, eventName, isDefaultD
824
870
  }
825
871
  chargeResult = results.reduce(mergeChargeResults);
826
872
  if (acceptedForItems <= 0) {
873
+ // Nothing will be stored (another event of this push was capped to
874
+ // zero): undo every platform charge this operation made, so no
875
+ // result is billed for items that are never saved.
876
+ for (let i = 0; i < reservedInOp.length; i++) {
877
+ const { name, operationId, uncertain, reserved } = reservedInOp[i];
878
+ if (reserved <= 0 || !operationId)
879
+ continue;
880
+ // Original count: the local reservation is released only when
881
+ // the platform confirms the void (or that it never applied).
882
+ if (uncertain)
883
+ await chargingManager._cancelUncertainCharge?.(name, operationId, reserved);
884
+ else
885
+ await chargingManager._voidCharge(name, operationId, reserved);
886
+ }
827
887
  return {
828
888
  eventChargeLimitReached: true,
829
889
  chargedCount: chargeResult.chargedCount,
@@ -848,11 +908,15 @@ async function pushDataAndCharge({ chargingManager, items, eventName, isDefaultD
848
908
  if (!pushCertainlyFailed(pushErr))
849
909
  throw pushErr;
850
910
  for (let i = 0; i < reservedInOp.length; i++) {
851
- const { name, operationId } = reservedInOp[i];
911
+ const { name, operationId, uncertain } = reservedInOp[i];
852
912
  const reserved = results[i].chargedCount;
853
913
  if (reserved <= 0)
854
914
  continue;
855
- if (operationId) {
915
+ if (operationId && uncertain) {
916
+ // Never re-post a charge for items that were not stored.
917
+ await chargingManager._cancelUncertainCharge?.(name, operationId, reserved);
918
+ }
919
+ else if (operationId) {
856
920
  await chargingManager._voidCharge(name, operationId, reserved);
857
921
  }
858
922
  else if (isSyntheticEvent(name)) {
@@ -895,3 +959,9 @@ function pushCertainlyFailed(err) {
895
959
  }
896
960
  return false;
897
961
  }
962
+ /** API error type (e.g. 'charge-not-found') of a client error, if the server sent one. */
963
+ function apiErrorTypeOf(err) {
964
+ const e = err;
965
+ const t = e?.type ?? e?.data?.error?.type ?? e?.response?.data?.error?.type;
966
+ return typeof t === 'string' ? t : undefined;
967
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scrapely",
3
- "version": "1.2.9",
3
+ "version": "1.2.10",
4
4
  "description": "Scrapely SDK for building JavaScript and TypeScript Actors",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",