scrapely 1.2.20 → 1.2.21

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.
@@ -77,6 +77,15 @@ export declare class ChargingManager {
77
77
  private chargeLimitHit;
78
78
  /** Default-dataset rows refused after the limit (one warning, then a count). */
79
79
  private refusedDefaultDatasetItems;
80
+ /**
81
+ * Prepaid-row balance: priced events charged through charge() minus rows
82
+ * pushed to the default dataset without a per-row charge. An Actor that
83
+ * charges first and then pushes plainly (charge('product'); pushData(row))
84
+ * has a positive balance after the charge, so the row it already paid for is
85
+ * still saved when that charge used up the spending limit. Push-then-charge
86
+ * Actors never go positive, so they get nothing extra for free.
87
+ */
88
+ private prepaidRowBalance;
80
89
  /** @internal Called by Actor shutdown before the final flush. */
81
90
  close(): void;
82
91
  /**
@@ -98,6 +107,18 @@ export declare class ChargingManager {
98
107
  /** @internal Whether every row of a push with these events is charged a positive price. */
99
108
  _isPaidPerItem(eventName: string | undefined, isDefaultDataset: boolean): boolean;
100
109
  /** @internal */
110
+ _notePrepaidCharge(eventName: string, chargedCount: number): void;
111
+ /** @internal */
112
+ _isPayPerEvent(): boolean;
113
+ /** @internal Current prepaid-row balance (tests). */
114
+ _prepaidRowBalance(): number;
115
+ /**
116
+ * @internal Account for rows pushed without a per-row charge. Returns how
117
+ * many may be stored: all while the limit is not reached, afterwards only
118
+ * the rows earlier charges already paid for (the positive balance).
119
+ */
120
+ _takeUnpaidRows(itemCount: number): number;
121
+ /** @internal */
101
122
  _refuseDefaultDatasetPush(itemCount: number): boolean;
102
123
  /** @internal Whether shutdown has closed this manager to new charges. */
103
124
  isClosed(): boolean;
package/dist/charging.js CHANGED
@@ -154,6 +154,15 @@ class ChargingManager {
154
154
  chargeLimitHit = false;
155
155
  /** Default-dataset rows refused after the limit (one warning, then a count). */
156
156
  refusedDefaultDatasetItems = 0;
157
+ /**
158
+ * Prepaid-row balance: priced events charged through charge() minus rows
159
+ * pushed to the default dataset without a per-row charge. An Actor that
160
+ * charges first and then pushes plainly (charge('product'); pushData(row))
161
+ * has a positive balance after the charge, so the row it already paid for is
162
+ * still saved when that charge used up the spending limit. Push-then-charge
163
+ * Actors never go positive, so they get nothing extra for free.
164
+ */
165
+ prepaidRowBalance = 0;
157
166
  /** @internal Called by Actor shutdown before the final flush. */
158
167
  close() {
159
168
  this._closed = true;
@@ -216,9 +225,49 @@ class ChargingManager {
216
225
  return names.some((name) => (this.calculateEventPrice(name) ?? 0) > 0);
217
226
  }
218
227
  /** @internal */
228
+ _notePrepaidCharge(eventName, chargedCount) {
229
+ if (chargedCount > 0 && !isSyntheticEvent(eventName) && (this.pricingInfo[eventName]?.price ?? 0) > 0) {
230
+ this.prepaidRowBalance += chargedCount;
231
+ }
232
+ }
233
+ /** @internal */
234
+ _isPayPerEvent() {
235
+ return this.chargingState !== undefined && this.isPayPerEvent;
236
+ }
237
+ /** @internal Current prepaid-row balance (tests). */
238
+ _prepaidRowBalance() {
239
+ return this.prepaidRowBalance;
240
+ }
241
+ /**
242
+ * @internal Account for rows pushed without a per-row charge. Returns how
243
+ * many may be stored: all while the limit is not reached, afterwards only
244
+ * the rows earlier charges already paid for (the positive balance).
245
+ */
246
+ _takeUnpaidRows(itemCount) {
247
+ if (itemCount <= 0)
248
+ return 0;
249
+ if (!this.isChargeLimitReached()) {
250
+ this.prepaidRowBalance -= itemCount;
251
+ return itemCount;
252
+ }
253
+ const allowed = Math.max(0, Math.min(itemCount, this.prepaidRowBalance));
254
+ this.prepaidRowBalance -= allowed;
255
+ return allowed;
256
+ }
257
+ /** @internal */
219
258
  _refuseDefaultDatasetPush(itemCount) {
220
- if (itemCount <= 0 || !this.isChargeLimitReached())
259
+ if (itemCount <= 0 || !this.isPayPerEvent)
260
+ return false;
261
+ if (!this.isChargeLimitReached()) {
262
+ this._takeUnpaidRows(itemCount);
221
263
  return false;
264
+ }
265
+ // All-or-nothing here (storage clients cannot store a slice): store the
266
+ // batch only when earlier charges paid for every row of it.
267
+ if (this.prepaidRowBalance >= itemCount) {
268
+ this.prepaidRowBalance -= itemCount;
269
+ return false;
270
+ }
222
271
  this._noteRefusedDefaultDatasetItems(itemCount);
223
272
  return true;
224
273
  }
@@ -771,7 +820,11 @@ class ChargingManager {
771
820
  }
772
821
  }
773
822
  async charge({ eventName, count = 1 }) {
774
- return this._withOperationLock(() => this._chargeExclusive({ eventName, count }));
823
+ return this._withOperationLock(async () => {
824
+ const result = await this._chargeExclusive({ eventName, count });
825
+ this._notePrepaidCharge(eventName, result.chargedCount);
826
+ return result;
827
+ });
775
828
  }
776
829
  /** @internal Caller must hold the operation lock. */
777
830
  async _chargeExclusive({ eventName, count = 1 }) {
@@ -983,16 +1036,27 @@ async function pushDataAndCharge({ chargingManager, items, eventName, isDefaultD
983
1036
  // default-dataset push that does not pay per row (no event, or only free
984
1037
  // events) stores nothing. Paid pushes go on below, where the budget cap
985
1038
  // stores only the rows that are actually charged.
1039
+ // Rows the Actor already paid for with an earlier charge() (charge first,
1040
+ // then a plain push) are still stored: see ChargingManager._takeUnpaidRows.
1041
+ // The result still reports the limit, so the Actor stops after them.
1042
+ let unpaidPastLimit = false;
986
1043
  if (isDefaultDataset &&
987
1044
  requested > 0 &&
988
- chargingManager.isChargeLimitReached?.() &&
1045
+ chargingManager._isPayPerEvent() &&
989
1046
  !chargingManager._isPaidPerItem(eventName, isDefaultDataset)) {
990
- chargingManager._noteRefusedDefaultDatasetItems(requested);
991
- return {
992
- eventChargeLimitReached: true,
993
- chargedCount: 0,
994
- chargeableWithinLimit: chargingManager._chargeableWithinLimit?.() ?? {},
995
- };
1047
+ unpaidPastLimit = chargingManager.isChargeLimitReached?.() ?? false;
1048
+ const allowed = chargingManager._takeUnpaidRows(requested);
1049
+ if (allowed < requested) {
1050
+ chargingManager._noteRefusedDefaultDatasetItems(requested - allowed);
1051
+ if (allowed === 0) {
1052
+ return {
1053
+ eventChargeLimitReached: true,
1054
+ chargedCount: 0,
1055
+ chargeableWithinLimit: chargingManager._chargeableWithinLimit?.() ?? {},
1056
+ };
1057
+ }
1058
+ items = (Array.isArray(items) ? items.slice(0, allowed) : items);
1059
+ }
996
1060
  }
997
1061
  const { limitedItems, eventsToCharge } = chargingManager.calculatePushDataLimits({
998
1062
  items,
@@ -1161,12 +1225,12 @@ async function pushDataAndCharge({ chargingManager, items, eventName, isDefaultD
1161
1225
  }
1162
1226
  throw pushErr;
1163
1227
  }
1164
- return chargeResult;
1228
+ return unpaidPastLimit ? { ...chargeResult, eventChargeLimitReached: true } : chargeResult;
1165
1229
  }
1166
1230
  // No billable events — still persist the budget-limited items.
1167
1231
  await chargeGatedPush.run(true, () => pushFn(Array.isArray(items) ? limitedItems : limitedItems[0]));
1168
1232
  return {
1169
- eventChargeLimitReached: false,
1233
+ eventChargeLimitReached: unpaidPastLimit,
1170
1234
  chargedCount: 0,
1171
1235
  chargeableWithinLimit: {},
1172
1236
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scrapely",
3
- "version": "1.2.20",
3
+ "version": "1.2.21",
4
4
  "description": "Scrapely SDK for building JavaScript and TypeScript Actors",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",