scrapely 1.2.14 → 1.2.16
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/actor.js +11 -0
- package/dist/charging.d.ts +35 -0
- package/dist/charging.js +150 -3
- package/dist/charging_client.js +5 -1
- package/dist/storage/api-storage-client.js +5 -0
- package/package.json +2 -1
package/dist/actor.js
CHANGED
|
@@ -273,6 +273,8 @@ class Actor {
|
|
|
273
273
|
apifyClientInstance = this._apifyClient;
|
|
274
274
|
apifyClientAssigned = true;
|
|
275
275
|
await this.chargingManager.init();
|
|
276
|
+
// Storage clients refuse default-dataset writes past this run's limit.
|
|
277
|
+
(0, charging_js_1.setActiveChargingManager)(this.chargingManager);
|
|
276
278
|
// Flush pending PPE intents before hard exit so uncertain charges can still settle.
|
|
277
279
|
const gracefulSignalExit = (signal) => {
|
|
278
280
|
const doExit = () => {
|
|
@@ -517,6 +519,14 @@ class Actor {
|
|
|
517
519
|
const { Dataset } = await import('crawlee');
|
|
518
520
|
const dataset = await Dataset.open();
|
|
519
521
|
if (eventName === undefined && !this.usesPushDataInterception(dataset)) {
|
|
522
|
+
// Charge-limit safety net (also for local storage): this push is not
|
|
523
|
+
// charged, so past the run's spending limit it is not stored.
|
|
524
|
+
if (this.initialized &&
|
|
525
|
+
dataset.id === this.config.get('defaultDatasetId') &&
|
|
526
|
+
this.chargingManager.isChargeLimitReached()) {
|
|
527
|
+
this.chargingManager._noteRefusedDefaultDatasetItems(Array.isArray(item) ? item.length : 1);
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
520
530
|
await dataset.pushData(item);
|
|
521
531
|
return;
|
|
522
532
|
}
|
|
@@ -951,6 +961,7 @@ class Actor {
|
|
|
951
961
|
await eventManager.close();
|
|
952
962
|
(0, events_client_js_1.disconnectEvents)();
|
|
953
963
|
(0, patch_storage_js_1.disconnectStorage)();
|
|
964
|
+
(0, charging_js_1.setActiveChargingManager)(undefined);
|
|
954
965
|
// Let buffered stdout/stderr reach the log pipe before the process exits
|
|
955
966
|
// (pipe writes are async in Node). Storage was flushed above, so this
|
|
956
967
|
// replaces the old fixed 1 s sleep: it resolves as soon as both streams
|
package/dist/charging.d.ts
CHANGED
|
@@ -29,6 +29,17 @@ export declare class ChargeRejectedError extends Error {
|
|
|
29
29
|
reason: string;
|
|
30
30
|
});
|
|
31
31
|
}
|
|
32
|
+
/** @internal Register the manager whose limit guards default-dataset writes. */
|
|
33
|
+
export declare function setActiveChargingManager(manager: ChargingManager | undefined): void;
|
|
34
|
+
/** @internal The run's default dataset ID, as the platform passes it. */
|
|
35
|
+
export declare function defaultDatasetIdFromEnv(): string | undefined;
|
|
36
|
+
/**
|
|
37
|
+
* @internal True when a write of `itemCount` rows to the default dataset must
|
|
38
|
+
* be dropped (pay-per-event run past its spending limit). Called by storage
|
|
39
|
+
* clients for writes that did not go through `pushDataAndCharge`
|
|
40
|
+
* (`Actor.openDataset().pushData()`, crawler `pushData`, ...).
|
|
41
|
+
*/
|
|
42
|
+
export declare function refuseDefaultDatasetPush(itemCount: number): boolean;
|
|
32
43
|
export declare const APIFY_DEFAULT_DATASET_ITEM_EVENT = "apify-default-dataset-item";
|
|
33
44
|
/** Scrapely-native synthetic event for default-dataset items. */
|
|
34
45
|
export declare const SCRAPELY_DEFAULT_DATASET_ITEM_EVENT = "scrapely-default-dataset-item";
|
|
@@ -62,8 +73,32 @@ export declare class ChargingManager {
|
|
|
62
73
|
private readonly apifyClient;
|
|
63
74
|
/** Set during actor shutdown; rejects new charge operations after the final flush. */
|
|
64
75
|
private _closed;
|
|
76
|
+
/** Sticky: the platform refused or capped a charge (HTTP 402 / fewer accepted) or the local gate trimmed one. */
|
|
77
|
+
private chargeLimitHit;
|
|
78
|
+
/** Default-dataset rows refused after the limit (one warning, then a count). */
|
|
79
|
+
private refusedDefaultDatasetItems;
|
|
65
80
|
/** @internal Called by Actor shutdown before the final flush. */
|
|
66
81
|
close(): void;
|
|
82
|
+
/**
|
|
83
|
+
* Whether this pay-per-event run has used up its spending limit.
|
|
84
|
+
*
|
|
85
|
+
* True once a charge was refused or capped by the limit (the platform's
|
|
86
|
+
* HTTP 402 or a partial acceptance, or the local budget gate), or once the
|
|
87
|
+
* remaining budget cannot pay for one event of the cheapest priced type.
|
|
88
|
+
* From then on, pushes to the default dataset that are not paid for are
|
|
89
|
+
* refused. Always false for actors without pay-per-event pricing, without
|
|
90
|
+
* a limit, or with only free events.
|
|
91
|
+
*/
|
|
92
|
+
isChargeLimitReached(): boolean;
|
|
93
|
+
private markChargeLimitHit;
|
|
94
|
+
/** @internal Count rows dropped because of the limit; warn on the first one only. */
|
|
95
|
+
_noteRefusedDefaultDatasetItems(count: number): void;
|
|
96
|
+
/** @internal */
|
|
97
|
+
_markChargeLimitHit(): void;
|
|
98
|
+
/** @internal Whether every row of a push with these events is charged a positive price. */
|
|
99
|
+
_isPaidPerItem(eventName: string | undefined, isDefaultDataset: boolean): boolean;
|
|
100
|
+
/** @internal */
|
|
101
|
+
_refuseDefaultDatasetPush(itemCount: number): boolean;
|
|
67
102
|
/** @internal Whether shutdown has closed this manager to new charges. */
|
|
68
103
|
isClosed(): boolean;
|
|
69
104
|
private readonly LOCAL_CHARGING_LOG_DATASET_NAME;
|
package/dist/charging.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ChargingManager = exports.DEFAULT_DATASET_ITEM_EVENT = exports.SCRAPELY_DEFAULT_DATASET_ITEM_EVENT = exports.APIFY_DEFAULT_DATASET_ITEM_EVENT = exports.ChargeRejectedError = exports.ChargeUncertainError = exports.CHARGE_IDEMPOTENCY_SAFE_AGE_MS = void 0;
|
|
4
|
+
exports.setActiveChargingManager = setActiveChargingManager;
|
|
5
|
+
exports.defaultDatasetIdFromEnv = defaultDatasetIdFromEnv;
|
|
6
|
+
exports.refuseDefaultDatasetPush = refuseDefaultDatasetPush;
|
|
4
7
|
exports.isDefaultDatasetItemEvent = isDefaultDatasetItemEvent;
|
|
5
8
|
exports.resolveDefaultDatasetItemEventName = resolveDefaultDatasetItemEventName;
|
|
6
9
|
exports.hasDefaultDatasetItemEventInPricing = hasDefaultDatasetItemEventInPricing;
|
|
@@ -11,6 +14,7 @@ exports.pushCertainlyFailed = pushCertainlyFailed;
|
|
|
11
14
|
// Derived from apify-sdk-js (src/charging.ts),
|
|
12
15
|
// Copyright Apify Technologies s.r.o., licensed under the Apache License 2.0.
|
|
13
16
|
// Modified by TeraShift GmbH. See LICENSE-APACHE and NOTICE.
|
|
17
|
+
const node_async_hooks_1 = require("node:async_hooks");
|
|
14
18
|
const node_crypto_1 = require("node:crypto");
|
|
15
19
|
/** Client safe-replay window; server TTL is longer (35d). */
|
|
16
20
|
exports.CHARGE_IDEMPOTENCY_SAFE_AGE_MS = 30 * 24 * 60 * 60 * 1000;
|
|
@@ -48,6 +52,44 @@ class ChargeRejectedError extends Error {
|
|
|
48
52
|
}
|
|
49
53
|
}
|
|
50
54
|
exports.ChargeRejectedError = ChargeRejectedError;
|
|
55
|
+
/** True when a ChargeRejectedError means the run's spending limit is used up. */
|
|
56
|
+
function isChargeLimitRejection(err) {
|
|
57
|
+
return (err.statusCode === 402 ||
|
|
58
|
+
err.reason === 'charge-limit-exceeded' ||
|
|
59
|
+
/charge-limit-exceeded/i.test(err.message));
|
|
60
|
+
}
|
|
61
|
+
/*
|
|
62
|
+
* Charge-limit safety net for the default dataset.
|
|
63
|
+
*
|
|
64
|
+
* Once a pay-per-event run has used up its spending limit (maxTotalChargeUsd),
|
|
65
|
+
* rows written to the run's default dataset would go out unbilled. The SDK then
|
|
66
|
+
* refuses such writes: nothing is stored, one warning is logged and
|
|
67
|
+
* `ChargingManager.isChargeLimitReached()` turns true. Pushes made by
|
|
68
|
+
* `pushDataAndCharge` are decided there and run inside `chargeGatedPush`, so
|
|
69
|
+
* the storage client does not check them twice.
|
|
70
|
+
*/
|
|
71
|
+
const chargeGatedPush = new node_async_hooks_1.AsyncLocalStorage();
|
|
72
|
+
/** The charging manager of the running Actor; storage clients consult it. */
|
|
73
|
+
let activeChargingManager;
|
|
74
|
+
/** @internal Register the manager whose limit guards default-dataset writes. */
|
|
75
|
+
function setActiveChargingManager(manager) {
|
|
76
|
+
activeChargingManager = manager;
|
|
77
|
+
}
|
|
78
|
+
/** @internal The run's default dataset ID, as the platform passes it. */
|
|
79
|
+
function defaultDatasetIdFromEnv() {
|
|
80
|
+
return process.env.ACTOR_DEFAULT_DATASET_ID || process.env.APIFY_DEFAULT_DATASET_ID || undefined;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* @internal True when a write of `itemCount` rows to the default dataset must
|
|
84
|
+
* be dropped (pay-per-event run past its spending limit). Called by storage
|
|
85
|
+
* clients for writes that did not go through `pushDataAndCharge`
|
|
86
|
+
* (`Actor.openDataset().pushData()`, crawler `pushData`, ...).
|
|
87
|
+
*/
|
|
88
|
+
function refuseDefaultDatasetPush(itemCount) {
|
|
89
|
+
if (chargeGatedPush.getStore())
|
|
90
|
+
return false;
|
|
91
|
+
return activeChargingManager?._refuseDefaultDatasetPush(itemCount) ?? false;
|
|
92
|
+
}
|
|
51
93
|
exports.APIFY_DEFAULT_DATASET_ITEM_EVENT = 'apify-default-dataset-item';
|
|
52
94
|
/** Scrapely-native synthetic event for default-dataset items. */
|
|
53
95
|
exports.SCRAPELY_DEFAULT_DATASET_ITEM_EVENT = 'scrapely-default-dataset-item';
|
|
@@ -95,9 +137,77 @@ class ChargingManager {
|
|
|
95
137
|
apifyClient;
|
|
96
138
|
/** Set during actor shutdown; rejects new charge operations after the final flush. */
|
|
97
139
|
_closed = false;
|
|
140
|
+
/** Sticky: the platform refused or capped a charge (HTTP 402 / fewer accepted) or the local gate trimmed one. */
|
|
141
|
+
chargeLimitHit = false;
|
|
142
|
+
/** Default-dataset rows refused after the limit (one warning, then a count). */
|
|
143
|
+
refusedDefaultDatasetItems = 0;
|
|
98
144
|
/** @internal Called by Actor shutdown before the final flush. */
|
|
99
145
|
close() {
|
|
100
146
|
this._closed = true;
|
|
147
|
+
if (this.refusedDefaultDatasetItems > 0) {
|
|
148
|
+
console.warn(`[Scrapely] ${this.refusedDefaultDatasetItems} result(s) pushed to the default dataset after the spending limit was reached were not saved`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Whether this pay-per-event run has used up its spending limit.
|
|
153
|
+
*
|
|
154
|
+
* True once a charge was refused or capped by the limit (the platform's
|
|
155
|
+
* HTTP 402 or a partial acceptance, or the local budget gate), or once the
|
|
156
|
+
* remaining budget cannot pay for one event of the cheapest priced type.
|
|
157
|
+
* From then on, pushes to the default dataset that are not paid for are
|
|
158
|
+
* refused. Always false for actors without pay-per-event pricing, without
|
|
159
|
+
* a limit, or with only free events.
|
|
160
|
+
*/
|
|
161
|
+
isChargeLimitReached() {
|
|
162
|
+
if (this.chargingState === undefined || !this.isPayPerEvent)
|
|
163
|
+
return false;
|
|
164
|
+
if (this.chargeLimitHit)
|
|
165
|
+
return true;
|
|
166
|
+
if (!Number.isFinite(this.maxTotalChargeUsd))
|
|
167
|
+
return false;
|
|
168
|
+
const prices = Object.values(this.pricingInfo)
|
|
169
|
+
.map(({ price }) => price)
|
|
170
|
+
.filter((price) => price > 0);
|
|
171
|
+
if (prices.length === 0)
|
|
172
|
+
return false;
|
|
173
|
+
// Same rounding as the budget gate, so 0.03 - 0.02 still pays a $0.01 event.
|
|
174
|
+
return this.calculateMaxChargesByPrice(Math.min(...prices)) <= 0;
|
|
175
|
+
}
|
|
176
|
+
markChargeLimitHit() {
|
|
177
|
+
if (this.isPayPerEvent)
|
|
178
|
+
this.chargeLimitHit = true;
|
|
179
|
+
}
|
|
180
|
+
/** @internal Count rows dropped because of the limit; warn on the first one only. */
|
|
181
|
+
_noteRefusedDefaultDatasetItems(count) {
|
|
182
|
+
if (count <= 0)
|
|
183
|
+
return;
|
|
184
|
+
if (this.refusedDefaultDatasetItems === 0) {
|
|
185
|
+
const limit = Number.isFinite(this.maxTotalChargeUsd) ? `$${this.maxTotalChargeUsd}` : 'the limit';
|
|
186
|
+
console.warn(`[Scrapely] The spending limit of this run (maxTotalChargeUsd = ${limit}) has been reached: ` +
|
|
187
|
+
'results pushed to the default dataset are no longer saved. The Actor should stop now ' +
|
|
188
|
+
'(check ChargeResult.eventChargeLimitReached or Actor.getChargingManager().isChargeLimitReached()).');
|
|
189
|
+
}
|
|
190
|
+
this.refusedDefaultDatasetItems += count;
|
|
191
|
+
}
|
|
192
|
+
/** @internal */
|
|
193
|
+
_markChargeLimitHit() {
|
|
194
|
+
this.markChargeLimitHit();
|
|
195
|
+
}
|
|
196
|
+
/** @internal Whether every row of a push with these events is charged a positive price. */
|
|
197
|
+
_isPaidPerItem(eventName, isDefaultDataset) {
|
|
198
|
+
const names = [];
|
|
199
|
+
if (eventName !== undefined)
|
|
200
|
+
names.push(eventName);
|
|
201
|
+
if (isDefaultDataset)
|
|
202
|
+
names.push(resolveDefaultDatasetItemEventName(this.pricingInfo));
|
|
203
|
+
return names.some((name) => (this.calculateEventPrice(name) ?? 0) > 0);
|
|
204
|
+
}
|
|
205
|
+
/** @internal */
|
|
206
|
+
_refuseDefaultDatasetPush(itemCount) {
|
|
207
|
+
if (itemCount <= 0 || !this.isChargeLimitReached())
|
|
208
|
+
return false;
|
|
209
|
+
this._noteRefusedDefaultDatasetItems(itemCount);
|
|
210
|
+
return true;
|
|
101
211
|
}
|
|
102
212
|
/** @internal Whether shutdown has closed this manager to new charges. */
|
|
103
213
|
isClosed() {
|
|
@@ -538,6 +648,8 @@ class ChargingManager {
|
|
|
538
648
|
try {
|
|
539
649
|
const accepted = await this.postChargeWithRetries(intent);
|
|
540
650
|
if (accepted !== null) {
|
|
651
|
+
if (accepted < intent.count)
|
|
652
|
+
this.markChargeLimitHit();
|
|
541
653
|
const eventPrice = this.getEventPricing(intent.eventName).price;
|
|
542
654
|
this.applyAcceptedCount(intent.eventName, intent.count, accepted, eventPrice);
|
|
543
655
|
this.pendingCharges.delete(intent.idempotencyKey);
|
|
@@ -546,6 +658,8 @@ class ChargingManager {
|
|
|
546
658
|
}
|
|
547
659
|
catch (err) {
|
|
548
660
|
if (err instanceof ChargeRejectedError) {
|
|
661
|
+
if (isChargeLimitRejection(err))
|
|
662
|
+
this.markChargeLimitHit();
|
|
549
663
|
const eventPrice = this.getEventPricing(intent.eventName).price;
|
|
550
664
|
this.applyAcceptedCount(intent.eventName, intent.count, 0, eventPrice);
|
|
551
665
|
this.pendingCharges.delete(intent.idempotencyKey);
|
|
@@ -613,6 +727,8 @@ class ChargingManager {
|
|
|
613
727
|
}
|
|
614
728
|
return 0;
|
|
615
729
|
})();
|
|
730
|
+
if (chargedCount < count)
|
|
731
|
+
this.markChargeLimitHit();
|
|
616
732
|
if (chargedCount === 0) {
|
|
617
733
|
return {
|
|
618
734
|
eventChargeLimitReached: count > 0,
|
|
@@ -655,6 +771,8 @@ class ChargingManager {
|
|
|
655
771
|
}
|
|
656
772
|
catch (err) {
|
|
657
773
|
if (err instanceof ChargeRejectedError) {
|
|
774
|
+
if (isChargeLimitRejection(err))
|
|
775
|
+
this.markChargeLimitHit();
|
|
658
776
|
// Server definitively rejected: unwind local reservation.
|
|
659
777
|
this.pendingCharges.delete(intent.idempotencyKey);
|
|
660
778
|
chargedCount = this.applyAcceptedCount(normalizedEventName, chargedCount, 0, eventPricing.price);
|
|
@@ -670,6 +788,9 @@ class ChargingManager {
|
|
|
670
788
|
this.pendingCharges.delete(intent.idempotencyKey);
|
|
671
789
|
if (accepted > 0)
|
|
672
790
|
this.lastAcceptedOperationId = intent.idempotencyKey;
|
|
791
|
+
// The platform capped the charge at the run's limit.
|
|
792
|
+
if (accepted < chargedCount)
|
|
793
|
+
this.markChargeLimitHit();
|
|
673
794
|
chargedCount = this.applyAcceptedCount(normalizedEventName, chargedCount, accepted, eventPricing.price);
|
|
674
795
|
}
|
|
675
796
|
else {
|
|
@@ -772,6 +893,22 @@ async function pushDataAndCharge({ chargingManager, items, eventName, isDefaultD
|
|
|
772
893
|
// A void that did not go through for an earlier failed push must land
|
|
773
894
|
// before this push charges again, or one unsaved result is billed twice.
|
|
774
895
|
const voidsConfirmed = (await chargingManager._retryPendingVoids?.()) !== false;
|
|
896
|
+
const requested = Array.isArray(items) ? items.length : 1;
|
|
897
|
+
// Charge-limit safety net: once the run's spending limit is used up, a
|
|
898
|
+
// default-dataset push that does not pay per row (no event, or only free
|
|
899
|
+
// events) stores nothing. Paid pushes go on below, where the budget cap
|
|
900
|
+
// stores only the rows that are actually charged.
|
|
901
|
+
if (isDefaultDataset &&
|
|
902
|
+
requested > 0 &&
|
|
903
|
+
chargingManager.isChargeLimitReached?.() &&
|
|
904
|
+
!chargingManager._isPaidPerItem(eventName, isDefaultDataset)) {
|
|
905
|
+
chargingManager._noteRefusedDefaultDatasetItems(requested);
|
|
906
|
+
return {
|
|
907
|
+
eventChargeLimitReached: true,
|
|
908
|
+
chargedCount: 0,
|
|
909
|
+
chargeableWithinLimit: chargingManager._chargeableWithinLimit?.() ?? {},
|
|
910
|
+
};
|
|
911
|
+
}
|
|
775
912
|
const { limitedItems, eventsToCharge } = chargingManager.calculatePushDataLimits({
|
|
776
913
|
items,
|
|
777
914
|
eventName,
|
|
@@ -787,6 +924,11 @@ async function pushDataAndCharge({ chargingManager, items, eventName, isDefaultD
|
|
|
787
924
|
}
|
|
788
925
|
const itemsArray = Array.isArray(items) ? items : [items];
|
|
789
926
|
const allItemsTrimmed = itemsArray.length > 0 && limitedItems.length === 0;
|
|
927
|
+
if (isDefaultDataset && limitedItems.length < itemsArray.length) {
|
|
928
|
+
// Rows beyond what the budget can pay for are dropped.
|
|
929
|
+
chargingManager._markChargeLimitHit?.();
|
|
930
|
+
chargingManager._noteRefusedDefaultDatasetItems?.(itemsArray.length - limitedItems.length);
|
|
931
|
+
}
|
|
790
932
|
if (limitedItems.length === 0) {
|
|
791
933
|
return {
|
|
792
934
|
eventChargeLimitReached: allItemsTrimmed,
|
|
@@ -872,7 +1014,12 @@ async function pushDataAndCharge({ chargingManager, items, eventName, isDefaultD
|
|
|
872
1014
|
};
|
|
873
1015
|
}
|
|
874
1016
|
}
|
|
875
|
-
|
|
1017
|
+
// Report rows charged, as Apify does for the pushed event: summing the
|
|
1018
|
+
// explicit and default-dataset events would count every row twice.
|
|
1019
|
+
chargeResult = { ...results.reduce(mergeChargeResults), chargedCount: acceptedForItems };
|
|
1020
|
+
if (isDefaultDataset && acceptedForItems < limitedItems.length) {
|
|
1021
|
+
chargingManager._noteRefusedDefaultDatasetItems?.(limitedItems.length - acceptedForItems);
|
|
1022
|
+
}
|
|
876
1023
|
if (acceptedForItems <= 0) {
|
|
877
1024
|
// Nothing will be stored (another event of this push was capped to
|
|
878
1025
|
// zero): undo every platform charge this operation made, so no
|
|
@@ -896,7 +1043,7 @@ async function pushDataAndCharge({ chargingManager, items, eventName, isDefaultD
|
|
|
896
1043
|
}
|
|
897
1044
|
const toPersist = limitedItems.slice(0, acceptedForItems);
|
|
898
1045
|
try {
|
|
899
|
-
await pushFn(Array.isArray(items) ? toPersist : toPersist[0]);
|
|
1046
|
+
await chargeGatedPush.run(true, () => pushFn(Array.isArray(items) ? toPersist : toPersist[0]));
|
|
900
1047
|
}
|
|
901
1048
|
catch (pushErr) {
|
|
902
1049
|
// Storage failed after the charge was accepted: a result is billed
|
|
@@ -932,7 +1079,7 @@ async function pushDataAndCharge({ chargingManager, items, eventName, isDefaultD
|
|
|
932
1079
|
return chargeResult;
|
|
933
1080
|
}
|
|
934
1081
|
// No billable events — still persist the budget-limited items.
|
|
935
|
-
await pushFn(Array.isArray(items) ? limitedItems : limitedItems[0]);
|
|
1082
|
+
await chargeGatedPush.run(true, () => pushFn(Array.isArray(items) ? limitedItems : limitedItems[0]));
|
|
936
1083
|
return {
|
|
937
1084
|
eventChargeLimitReached: false,
|
|
938
1085
|
chargedCount: 0,
|
package/dist/charging_client.js
CHANGED
|
@@ -17,7 +17,11 @@ function createChargingHooks(actor) {
|
|
|
17
17
|
// Called by the client at `dataset()` time, so pricing is consulted
|
|
18
18
|
// lazily — same timing as the former subclass override.
|
|
19
19
|
shouldChargeDatasetPushes() {
|
|
20
|
-
|
|
20
|
+
const chargingManager = actor.getChargingManager();
|
|
21
|
+
// Past the spending limit, route default-dataset pushes through
|
|
22
|
+
// pushDataAndCharge too, which refuses rows nobody pays for.
|
|
23
|
+
return ((0, charging_js_1.hasDefaultDatasetItemEventInPricing)(chargingManager.getPricingInfo().perEventPrices) ||
|
|
24
|
+
(chargingManager.isChargeLimitReached?.() ?? false));
|
|
21
25
|
},
|
|
22
26
|
async pushAndCharge({ items, eventName, pushFn }) {
|
|
23
27
|
return (0, charging_js_1.pushDataAndCharge)({
|
|
@@ -5,6 +5,7 @@ exports.isRetryableResponse = isRetryableResponse;
|
|
|
5
5
|
exports.serverRetryDelayMs = serverRetryDelayMs;
|
|
6
6
|
exports.isConnectFailure = isConnectFailure;
|
|
7
7
|
const node_crypto_1 = require("node:crypto");
|
|
8
|
+
const charging_js_1 = require("../charging.js");
|
|
8
9
|
const kvs_iteration_js_1 = require("./kvs-iteration.js");
|
|
9
10
|
function buildRqQueryParams(baseClientKey, options) {
|
|
10
11
|
const params = new URLSearchParams();
|
|
@@ -559,6 +560,10 @@ class ApiDatasetClient extends BaseApiClient {
|
|
|
559
560
|
else {
|
|
560
561
|
parsed = [items];
|
|
561
562
|
}
|
|
563
|
+
// Charge-limit safety net: a pay-per-event run past its spending limit
|
|
564
|
+
// must not store unbilled rows in its default dataset.
|
|
565
|
+
if (this.id === (0, charging_js_1.defaultDatasetIdFromEnv)() && (0, charging_js_1.refuseDefaultDatasetPush)(parsed.length))
|
|
566
|
+
return;
|
|
562
567
|
await this.request('POST', `/datasets/${this.id}/items`, parsed);
|
|
563
568
|
}
|
|
564
569
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "scrapely",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.16",
|
|
4
4
|
"description": "Scrapely SDK for building JavaScript and TypeScript Actors",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"scripts": {
|
|
23
23
|
"test": "npx tsx tests/test-mvp.ts",
|
|
24
24
|
"test:charging": "npx tsx --test tests/charging-pending.test.ts",
|
|
25
|
+
"test:charge-limit-guard": "npx tsx --test tests/charge-limit-guard.test.ts",
|
|
25
26
|
"test:storage-retries": "npx tsx --test tests/storage-retries.test.ts",
|
|
26
27
|
"test:stdio-drain": "npx tsx --test tests/stdio-drain.test.ts",
|
|
27
28
|
"test:charging-client": "npx tsx --test tests/charging-client.test.ts",
|