scrapely 1.2.32 → 1.2.34
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/CHANGELOG.md +7 -0
- package/NOTICE +12 -26
- package/README.md +1 -2
- package/dist/actor.d.ts +32 -25
- package/dist/actor.js +39 -23
- package/dist/charging.d.ts +49 -4
- package/dist/charging.js +293 -44
- package/dist/configuration.js +2 -2
- package/dist/index.js +1 -1
- package/dist/node-wrapper.js +1 -1
- package/dist/patch-browser-pool.js +7 -3
- package/dist/proxy-configuration.d.ts +11 -11
- package/dist/proxy-configuration.js +1 -1
- package/dist/proxy-input-aliases.d.ts +1 -1
- package/dist/proxy-input-aliases.js +1 -1
- package/dist/request-list.d.ts +2 -2
- package/dist/request-list.js +1 -1
- package/dist/scrapely-proxy-configuration.d.ts +15 -16
- package/dist/scrapely-proxy-configuration.js +27 -31
- package/dist/solver.js +1 -1
- package/dist/storage/api-storage-client.d.ts +4 -5
- package/dist/storage/api-storage-client.js +17 -15
- package/dist/storage/billing-tracker.d.ts +1 -1
- package/dist/storage/billing-tracker.js +1 -1
- package/dist/storage/key-value-store-client.d.ts +0 -3
- package/dist/storage/key-value-store-client.js +2 -5
- package/dist/storage/kv-store-collection-client.js +6 -2
- package/dist/storage/kvs-iteration.d.ts +1 -1
- package/dist/storage/kvs-iteration.js +1 -1
- package/dist/storage/request-queue-client.d.ts +1 -1
- package/dist/storage/request-queue-client.js +5 -5
- package/dist/storage/rq-lock-script.d.ts +2 -2
- package/dist/storage/rq-lock-script.js +2 -2
- package/dist/storage/run-metadata.js +1 -1
- package/dist/storage/ttl-manager.d.ts +7 -6
- package/dist/storage/ttl-manager.js +5 -4
- package/dist/storage-identifiers.d.ts +1 -2
- package/dist/storage-identifiers.js +1 -2
- package/dist/utils.d.ts +41 -8
- package/dist/utils.js +49 -3
- package/package.json +22 -11
- package/dist/runner/docker-manager.d.ts +0 -88
- package/dist/runner/docker-manager.js +0 -283
- package/dist/runner/index.d.ts +0 -2
- package/dist/runner/index.js +0 -7
- package/dist/runner/timeout-manager.d.ts +0 -43
- package/dist/runner/timeout-manager.js +0 -90
package/dist/charging.js
CHANGED
|
@@ -8,6 +8,7 @@ exports.isDefaultDatasetItemEvent = isDefaultDatasetItemEvent;
|
|
|
8
8
|
exports.resolveDefaultDatasetItemEventName = resolveDefaultDatasetItemEventName;
|
|
9
9
|
exports.hasDefaultDatasetItemEventInPricing = hasDefaultDatasetItemEventInPricing;
|
|
10
10
|
exports.mergeChargeResults = mergeChargeResults;
|
|
11
|
+
exports.assertItemsSerializable = assertItemsSerializable;
|
|
11
12
|
exports.pushDataAndCharge = pushDataAndCharge;
|
|
12
13
|
exports.validateRowCharges = validateRowCharges;
|
|
13
14
|
exports.pushDataWithCharges = pushDataWithCharges;
|
|
@@ -48,7 +49,7 @@ class ChargeUncertainError extends Error {
|
|
|
48
49
|
}
|
|
49
50
|
}
|
|
50
51
|
exports.ChargeUncertainError = ChargeUncertainError;
|
|
51
|
-
/**
|
|
52
|
+
/** Synthetic event for default-dataset items in pay-per-event actors. */
|
|
52
53
|
/** Definitive platform rejection (402/400/409). Do not retry with the same key. */
|
|
53
54
|
class ChargeRejectedError extends Error {
|
|
54
55
|
operationId;
|
|
@@ -108,7 +109,7 @@ function refuseDefaultDatasetPush(itemCount) {
|
|
|
108
109
|
exports.APIFY_DEFAULT_DATASET_ITEM_EVENT = 'apify-default-dataset-item';
|
|
109
110
|
/** Scrapely-native synthetic event for default-dataset items. */
|
|
110
111
|
exports.SCRAPELY_DEFAULT_DATASET_ITEM_EVENT = 'scrapely-default-dataset-item';
|
|
111
|
-
/**
|
|
112
|
+
/** Alias — imported actors expect this constant name. */
|
|
112
113
|
exports.DEFAULT_DATASET_ITEM_EVENT = exports.APIFY_DEFAULT_DATASET_ITEM_EVENT;
|
|
113
114
|
function isDefaultDatasetItemEvent(eventName) {
|
|
114
115
|
return eventName === exports.APIFY_DEFAULT_DATASET_ITEM_EVENT || eventName === exports.SCRAPELY_DEFAULT_DATASET_ITEM_EVENT;
|
|
@@ -128,7 +129,7 @@ function hasDefaultDatasetItemEventInPricing(perEventPrices) {
|
|
|
128
129
|
}
|
|
129
130
|
/**
|
|
130
131
|
* Events that must never be POSTed to the charge API (server derives them at settle).
|
|
131
|
-
* Explicit names only — a prefix heuristic would drop real priced events
|
|
132
|
+
* Explicit names only — a prefix heuristic would drop real priced events.
|
|
132
133
|
*/
|
|
133
134
|
function isSyntheticEvent(eventName) {
|
|
134
135
|
return isDefaultDatasetItemEvent(eventName);
|
|
@@ -361,8 +362,23 @@ class ChargingManager {
|
|
|
361
362
|
lastAcceptedOperationId;
|
|
362
363
|
/** Voids not yet confirmed by the platform; retried before the next push and at exit. */
|
|
363
364
|
pendingVoids = [];
|
|
365
|
+
/**
|
|
366
|
+
* @internal Platform charges for results that were not stored and that the
|
|
367
|
+
* platform definitively refused to void (event -> count). They stay billed,
|
|
368
|
+
* so the next push of that event uses them instead of charging again.
|
|
369
|
+
*/
|
|
370
|
+
_voidRefusedCredit = {};
|
|
364
371
|
/** Unresolved platform POSTs: key = idempotency UUID (never event name). */
|
|
365
372
|
pendingCharges = new Map();
|
|
373
|
+
/**
|
|
374
|
+
* Record in the run's default key-value store that mirrors pendingCharges,
|
|
375
|
+
* so a container crash does not lose them: settlement re-applies them under
|
|
376
|
+
* their own idempotency keys. One record per container, so a restarted
|
|
377
|
+
* container never overwrites its predecessor's.
|
|
378
|
+
*/
|
|
379
|
+
pendingRecordKey = `SCRAPELY_PENDING_CHARGES-${(0, node_crypto_1.randomUUID)().replace(/-/g, '').slice(0, 12)}`;
|
|
380
|
+
/** Intent keys last written to that record ('' = nothing written / empty). */
|
|
381
|
+
persistedPendingSig = '';
|
|
366
382
|
flushInFlight = null;
|
|
367
383
|
operationTail = Promise.resolve();
|
|
368
384
|
postAttempts = POST_ATTEMPTS;
|
|
@@ -735,6 +751,39 @@ class ChargingManager {
|
|
|
735
751
|
this.lastAcceptedOperationId = undefined;
|
|
736
752
|
return id;
|
|
737
753
|
}
|
|
754
|
+
/**
|
|
755
|
+
* @internal Mirror the unconfirmed charges to the run's default key-value
|
|
756
|
+
* store (best effort, only when the set changed since the last write).
|
|
757
|
+
*/
|
|
758
|
+
async _persistPendingCharges() {
|
|
759
|
+
if (!this.isAtHome || !this.actorRunId)
|
|
760
|
+
return;
|
|
761
|
+
const intents = [...this.pendingCharges.values()].map(({ idempotencyKey, eventName, count, createdAt }) => ({ idempotencyKey, eventName, count, createdAt }));
|
|
762
|
+
const sig = intents.map((i) => i.idempotencyKey).sort().join(',');
|
|
763
|
+
if (sig === this.persistedPendingSig)
|
|
764
|
+
return;
|
|
765
|
+
const storeId = process.env.ACTOR_DEFAULT_KEY_VALUE_STORE_ID
|
|
766
|
+
|| process.env.APIFY_DEFAULT_KEY_VALUE_STORE_ID
|
|
767
|
+
|| this.configuration.get('defaultKeyValueStoreId');
|
|
768
|
+
if (!storeId || storeId === 'default')
|
|
769
|
+
return;
|
|
770
|
+
let timer;
|
|
771
|
+
try {
|
|
772
|
+
const store = this.apifyClient.keyValueStore(storeId);
|
|
773
|
+
await Promise.race([
|
|
774
|
+
store.setRecord({ key: this.pendingRecordKey, value: { v: 1, intents } }),
|
|
775
|
+
new Promise((_, reject) => { timer = setTimeout(() => reject(new Error('timed out')), 15_000); }),
|
|
776
|
+
]);
|
|
777
|
+
this.persistedPendingSig = sig;
|
|
778
|
+
}
|
|
779
|
+
catch (err) {
|
|
780
|
+
console.warn(`[Scrapely] Could not persist ${intents.length} unconfirmed charge(s): ${err.message}`);
|
|
781
|
+
}
|
|
782
|
+
finally {
|
|
783
|
+
if (timer)
|
|
784
|
+
clearTimeout(timer);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
738
787
|
/**
|
|
739
788
|
* @internal Reverse an accepted charge whose items were never stored
|
|
740
789
|
* (POST /v2/actor-runs/:runId/charge/void), then unwind the local
|
|
@@ -748,7 +797,54 @@ class ChargingManager {
|
|
|
748
797
|
*/
|
|
749
798
|
async _cancelUncertainCharge(eventName, operationId, reservedCount) {
|
|
750
799
|
this.pendingCharges.delete(operationId);
|
|
751
|
-
|
|
800
|
+
const voided = await this._voidCharge(eventName, operationId, reservedCount, true);
|
|
801
|
+
// A persisted copy is harmless either way (the void leaves a tombstone),
|
|
802
|
+
// but keep the record small.
|
|
803
|
+
await this._persistPendingCharges();
|
|
804
|
+
return voided;
|
|
805
|
+
}
|
|
806
|
+
/**
|
|
807
|
+
* @internal Void (or cancel, when unconfirmed) a whole charge and say what
|
|
808
|
+
* happened: 'voided' (confirmed, local reservation released), 'stands' (a
|
|
809
|
+
* definitive answer that it cannot be voided) or 'unknown' (queued for retry,
|
|
810
|
+
* reservation kept).
|
|
811
|
+
*/
|
|
812
|
+
async _voidOutcome(eventName, operationId, reservedCount, uncertain) {
|
|
813
|
+
const queued = this.pendingVoids.length;
|
|
814
|
+
const ok = uncertain
|
|
815
|
+
? await this._cancelUncertainCharge(eventName, operationId, reservedCount)
|
|
816
|
+
: await this._voidCharge(eventName, operationId, reservedCount);
|
|
817
|
+
if (ok)
|
|
818
|
+
return 'voided';
|
|
819
|
+
return this.pendingVoids.length > queued ? 'unknown' : 'stands';
|
|
820
|
+
}
|
|
821
|
+
/** @internal A charge for `count` unstored results stands (void refused): keep it as credit. */
|
|
822
|
+
_noteUnvoidable(eventName, count) {
|
|
823
|
+
if (count <= 0)
|
|
824
|
+
return;
|
|
825
|
+
this._voidRefusedCredit[eventName] = (this._voidRefusedCredit[eventName] ?? 0) + count;
|
|
826
|
+
console.warn(`[Scrapely] Charge for ${count} unstored '${eventName}' result(s) could not be voided; ` +
|
|
827
|
+
`the next push of '${eventName}' uses it instead of charging again`);
|
|
828
|
+
}
|
|
829
|
+
/** @internal Use credit for a whole charge of `count` events (all or nothing); returns the count used. */
|
|
830
|
+
_takeVoidCredit(eventName, count) {
|
|
831
|
+
const have = this._voidRefusedCredit[eventName] ?? 0;
|
|
832
|
+
if (count <= 0 || have < count)
|
|
833
|
+
return 0;
|
|
834
|
+
this._voidRefusedCredit[eventName] = have - count;
|
|
835
|
+
return count;
|
|
836
|
+
}
|
|
837
|
+
/** @internal */
|
|
838
|
+
_returnVoidCredit(eventName, count) {
|
|
839
|
+
if (count > 0)
|
|
840
|
+
this._voidRefusedCredit[eventName] = (this._voidRefusedCredit[eventName] ?? 0) + count;
|
|
841
|
+
}
|
|
842
|
+
/** @internal Void (or cancel) a charge for results that were not stored; a refused void becomes credit. */
|
|
843
|
+
async _voidOrCredit(eventName, operationId, reservedCount, uncertain) {
|
|
844
|
+
const outcome = await this._voidOutcome(eventName, operationId, reservedCount, uncertain);
|
|
845
|
+
if (outcome === 'stands')
|
|
846
|
+
this._noteUnvoidable(eventName, reservedCount);
|
|
847
|
+
return outcome;
|
|
752
848
|
}
|
|
753
849
|
async _voidCharge(eventName, operationId, reservedCount, notFoundMeansVoided = false, deadline) {
|
|
754
850
|
if (!this.isAtHome || !this.actorRunId)
|
|
@@ -897,6 +993,7 @@ class ChargingManager {
|
|
|
897
993
|
if (confirmed === 0)
|
|
898
994
|
break;
|
|
899
995
|
}
|
|
996
|
+
await this._persistPendingCharges();
|
|
900
997
|
return confirmedTotal;
|
|
901
998
|
})();
|
|
902
999
|
try {
|
|
@@ -1020,6 +1117,9 @@ class ChargingManager {
|
|
|
1020
1117
|
this.syncPlatformBudget();
|
|
1021
1118
|
}
|
|
1022
1119
|
else {
|
|
1120
|
+
// Not confirmed: keep a durable copy so a crash before the next
|
|
1121
|
+
// flush cannot lose it (settlement re-applies it).
|
|
1122
|
+
await this._persistPendingCharges();
|
|
1023
1123
|
throw new ChargeUncertainError(`Charge for event '${normalizedEventName}' (count=${chargedCount}) was not confirmed by the platform; operationId=${intent.idempotencyKey}`, {
|
|
1024
1124
|
operationId: intent.idempotencyKey,
|
|
1025
1125
|
eventName: normalizedEventName,
|
|
@@ -1064,7 +1164,7 @@ function extractChargeResponseBody(response) {
|
|
|
1064
1164
|
if (!response || typeof response !== 'object')
|
|
1065
1165
|
return {};
|
|
1066
1166
|
const r = response;
|
|
1067
|
-
//
|
|
1167
|
+
// The API client's httpClient.call returns { data, statusCode, ... }
|
|
1068
1168
|
if (r.data && typeof r.data === 'object' && !Array.isArray(r.data)) {
|
|
1069
1169
|
return r.data;
|
|
1070
1170
|
}
|
|
@@ -1099,6 +1199,12 @@ function classifyChargeHttpError(err, intent) {
|
|
|
1099
1199
|
reason: status === 402 ? 'charge-limit-exceeded' : `http-${status}`,
|
|
1100
1200
|
});
|
|
1101
1201
|
}
|
|
1202
|
+
// A 5xx / 408 / 429 answer is never a definitive rejection, whatever its text
|
|
1203
|
+
// says: the charge may have applied, and only a retry with the same
|
|
1204
|
+
// Idempotency-Key can tell (classifying it as rejected unwound the local
|
|
1205
|
+
// count and the caller charged again under a new key).
|
|
1206
|
+
if (status !== undefined && isTransientStatus(status))
|
|
1207
|
+
return null;
|
|
1102
1208
|
const message = String(err?.message ?? err ?? '');
|
|
1103
1209
|
if (/charge-limit-exceeded|unknown-event|ledger-sealed|invalid-input|conflict/i.test(message)) {
|
|
1104
1210
|
return new ChargeRejectedError(message, {
|
|
@@ -1111,7 +1217,39 @@ function classifyChargeHttpError(err, intent) {
|
|
|
1111
1217
|
}
|
|
1112
1218
|
return null;
|
|
1113
1219
|
}
|
|
1114
|
-
|
|
1220
|
+
/**
|
|
1221
|
+
* @internal Fail before anything is charged when an item cannot be serialized
|
|
1222
|
+
* for the dataset (circular reference, BigInt, a bare function or undefined):
|
|
1223
|
+
* the push would otherwise fail after the charge, with nothing stored.
|
|
1224
|
+
*/
|
|
1225
|
+
function assertItemsSerializable(items) {
|
|
1226
|
+
items.forEach((item, index) => {
|
|
1227
|
+
let json;
|
|
1228
|
+
try {
|
|
1229
|
+
json = JSON.stringify(item);
|
|
1230
|
+
}
|
|
1231
|
+
catch (err) {
|
|
1232
|
+
throw new Error(`Dataset item at index ${index} is not serializable to JSON: ${err.message}`);
|
|
1233
|
+
}
|
|
1234
|
+
if (json === undefined)
|
|
1235
|
+
throw new Error(`Dataset item at index ${index} is not serializable to JSON`);
|
|
1236
|
+
});
|
|
1237
|
+
}
|
|
1238
|
+
// Credit / void helpers that also work with partial managers (tests, older shims).
|
|
1239
|
+
const takeVoidCredit = (m, name, count) => isSyntheticEvent(name) ? 0 : (m._takeVoidCredit?.(name, count) ?? 0);
|
|
1240
|
+
const returnVoidCredit = (m, name, count) => m._returnVoidCredit?.(name, count);
|
|
1241
|
+
async function voidOrCredit(m, name, operationId, reserved, uncertain, creditIfStands = true) {
|
|
1242
|
+
if (creditIfStands && m._voidOrCredit)
|
|
1243
|
+
return m._voidOrCredit(name, operationId, reserved, uncertain);
|
|
1244
|
+
if (m._voidOutcome)
|
|
1245
|
+
return m._voidOutcome(name, operationId, reserved, uncertain);
|
|
1246
|
+
if (uncertain)
|
|
1247
|
+
await m._cancelUncertainCharge?.(name, operationId, reserved);
|
|
1248
|
+
else
|
|
1249
|
+
await m._voidCharge(name, operationId, reserved);
|
|
1250
|
+
return 'voided';
|
|
1251
|
+
}
|
|
1252
|
+
async function pushDataAndCharge({ chargingManager, items, eventName, isDefaultDataset, pushFn, validateItems, }) {
|
|
1115
1253
|
return chargingManager._withOperationLock(async () => {
|
|
1116
1254
|
if (chargingManager.isClosed()) {
|
|
1117
1255
|
throw new Error('ChargingManager is closed (actor is shutting down); charge rejected');
|
|
@@ -1178,6 +1316,9 @@ async function pushDataAndCharge({ chargingManager, items, eventName, isDefaultD
|
|
|
1178
1316
|
// items in the dataset. Operation lock is held across both steps.
|
|
1179
1317
|
let chargeResult;
|
|
1180
1318
|
if (Object.keys(eventsToCharge).length > 0) {
|
|
1319
|
+
// An item the storage would refuse fails here, before it is charged.
|
|
1320
|
+
assertItemsSerializable(limitedItems);
|
|
1321
|
+
validateItems?.(limitedItems);
|
|
1181
1322
|
const results = [];
|
|
1182
1323
|
// Synthetic/default-dataset events first (local-only); then explicit.
|
|
1183
1324
|
const ordered = Object.entries(eventsToCharge).sort(([a], [b]) => {
|
|
@@ -1185,10 +1326,35 @@ async function pushDataAndCharge({ chargingManager, items, eventName, isDefaultD
|
|
|
1185
1326
|
const bSyn = isSyntheticEvent(b) ? 0 : 1;
|
|
1186
1327
|
return aSyn - bSyn;
|
|
1187
1328
|
});
|
|
1329
|
+
// credit: events of this push paid from credit (an earlier charge that could not be voided).
|
|
1188
1330
|
const reservedInOp = [];
|
|
1331
|
+
// Undo charge i of this operation: void a platform charge, unwind a local
|
|
1332
|
+
// one, give credit back. A refused void becomes credit (its results were
|
|
1333
|
+
// not stored) unless the caller handles that itself (creditIfStands=false).
|
|
1334
|
+
const undoOp = async (i, creditIfStands = true) => {
|
|
1335
|
+
const entry = reservedInOp[i];
|
|
1336
|
+
const { name, reserved, operationId, uncertain } = entry;
|
|
1337
|
+
if (reserved <= 0)
|
|
1338
|
+
return 'voided';
|
|
1339
|
+
if (entry.credit) {
|
|
1340
|
+
returnVoidCredit(chargingManager, name, entry.credit);
|
|
1341
|
+
entry.credit = 0;
|
|
1342
|
+
return 'voided';
|
|
1343
|
+
}
|
|
1344
|
+
if (operationId)
|
|
1345
|
+
return voidOrCredit(chargingManager, name, operationId, reserved, !!uncertain, creditIfStands);
|
|
1346
|
+
chargingManager.applyAcceptedCount(name, reserved, 0);
|
|
1347
|
+
return 'voided';
|
|
1348
|
+
};
|
|
1189
1349
|
try {
|
|
1190
1350
|
for (const [name, chargeCount] of ordered) {
|
|
1191
1351
|
chargingManager._takeLastAcceptedOperationId?.();
|
|
1352
|
+
const credit = takeVoidCredit(chargingManager, name, chargeCount);
|
|
1353
|
+
if (credit > 0) {
|
|
1354
|
+
results.push({ eventChargeLimitReached: false, chargedCount: credit, chargeableWithinLimit: chargingManager._chargeableWithinLimit?.() ?? {} });
|
|
1355
|
+
reservedInOp.push({ name, reserved: credit, credit });
|
|
1356
|
+
continue;
|
|
1357
|
+
}
|
|
1192
1358
|
let r;
|
|
1193
1359
|
try {
|
|
1194
1360
|
r = await chargingManager._chargeExclusive({
|
|
@@ -1218,14 +1384,64 @@ async function pushDataAndCharge({ chargingManager, items, eventName, isDefaultD
|
|
|
1218
1384
|
}
|
|
1219
1385
|
}
|
|
1220
1386
|
catch (err) {
|
|
1221
|
-
//
|
|
1222
|
-
//
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1387
|
+
// Failure after a partial multi-event reserve: undo every prior
|
|
1388
|
+
// charge of this operation, voiding the ones the platform accepted
|
|
1389
|
+
// (not only unwinding them locally).
|
|
1390
|
+
for (let i = 0; i < reservedInOp.length; i++)
|
|
1391
|
+
await undoOp(i);
|
|
1392
|
+
throw err;
|
|
1393
|
+
}
|
|
1394
|
+
// One platform event accepted more than another: only the smaller
|
|
1395
|
+
// count of items is stored, so the larger platform charge is voided
|
|
1396
|
+
// and charged again for exactly that count. A charge that cannot be
|
|
1397
|
+
// voided (definitive answer) stays as before; one whose void is not
|
|
1398
|
+
// confirmed yet fails the push so the caller retries it.
|
|
1399
|
+
// (Today a push posts at most one platform event; this keeps it
|
|
1400
|
+
// correct if that changes.)
|
|
1401
|
+
const stood = new Set();
|
|
1402
|
+
for (let round = 0; round < 3; round++) {
|
|
1403
|
+
const acceptedNow = Math.min(limitedItems.length, ...results.map((r) => r.chargedCount));
|
|
1404
|
+
const excess = results
|
|
1405
|
+
.map((r, i) => i)
|
|
1406
|
+
.filter((i) => reservedInOp[i]?.operationId && !stood.has(i) && results[i].chargedCount > acceptedNow);
|
|
1407
|
+
if (acceptedNow <= 0 || excess.length === 0)
|
|
1408
|
+
break;
|
|
1409
|
+
for (const i of excess) {
|
|
1410
|
+
const name = reservedInOp[i].name;
|
|
1411
|
+
// A refused void here keeps its stored share; the rest becomes credit below.
|
|
1412
|
+
const outcome = await undoOp(i, false);
|
|
1413
|
+
if (outcome === 'stands') {
|
|
1414
|
+
stood.add(i);
|
|
1415
|
+
continue;
|
|
1416
|
+
}
|
|
1417
|
+
if (outcome === 'unknown') {
|
|
1418
|
+
for (let j = 0; j < reservedInOp.length; j++)
|
|
1419
|
+
if (j !== i)
|
|
1420
|
+
await undoOp(j);
|
|
1421
|
+
throw new Error('[Scrapely] An earlier charge for a failed dataset push could not be voided yet; retry the push');
|
|
1422
|
+
}
|
|
1423
|
+
chargingManager._takeLastAcceptedOperationId?.();
|
|
1424
|
+
let r;
|
|
1425
|
+
let operationId;
|
|
1426
|
+
let uncertain = false;
|
|
1427
|
+
try {
|
|
1428
|
+
r = await chargingManager._chargeExclusive({ eventName: name, count: acceptedNow });
|
|
1429
|
+
operationId = chargingManager._takeLastAcceptedOperationId?.();
|
|
1430
|
+
}
|
|
1431
|
+
catch (chargeErr) {
|
|
1432
|
+
if (!(chargeErr instanceof ChargeUncertainError))
|
|
1433
|
+
throw chargeErr;
|
|
1434
|
+
r = {
|
|
1435
|
+
eventChargeLimitReached: false,
|
|
1436
|
+
chargedCount: chargeErr.count,
|
|
1437
|
+
chargeableWithinLimit: chargingManager._chargeableWithinLimit?.() ?? {},
|
|
1438
|
+
};
|
|
1439
|
+
operationId = chargeErr.operationId;
|
|
1440
|
+
uncertain = true;
|
|
1226
1441
|
}
|
|
1442
|
+
results[i] = r;
|
|
1443
|
+
reservedInOp[i] = { name, reserved: r.chargedCount, operationId, uncertain };
|
|
1227
1444
|
}
|
|
1228
|
-
throw err;
|
|
1229
1445
|
}
|
|
1230
1446
|
// Persist only what every charged event accepted.
|
|
1231
1447
|
// When multiple events are charged for the same items, use the
|
|
@@ -1243,8 +1459,22 @@ async function pushDataAndCharge({ chargingManager, items, eventName, isDefaultD
|
|
|
1243
1459
|
// count to the void below, which releases it only once the
|
|
1244
1460
|
// platform confirms (never on a server without the void route).
|
|
1245
1461
|
const platformOp = acceptedForItems <= 0 && reservedInOp[i]?.operationId;
|
|
1246
|
-
if (!platformOp)
|
|
1247
|
-
|
|
1462
|
+
if (!platformOp) {
|
|
1463
|
+
const entry = reservedInOp[i];
|
|
1464
|
+
let over = reserved - acceptedForItems;
|
|
1465
|
+
const back = Math.min(over, entry.credit ?? 0);
|
|
1466
|
+
if (back > 0) {
|
|
1467
|
+
returnVoidCredit(chargingManager, name, back);
|
|
1468
|
+
entry.credit = (entry.credit ?? 0) - back;
|
|
1469
|
+
entry.reserved -= back;
|
|
1470
|
+
over -= back;
|
|
1471
|
+
}
|
|
1472
|
+
// The platform refused to void it: the unstored share stays billed.
|
|
1473
|
+
if (over > 0 && stood.has(i))
|
|
1474
|
+
chargingManager._noteUnvoidable?.(name, over);
|
|
1475
|
+
else if (over > 0)
|
|
1476
|
+
chargingManager.applyAcceptedCount(name, acceptedForItems + over, acceptedForItems);
|
|
1477
|
+
}
|
|
1248
1478
|
results[i] = {
|
|
1249
1479
|
...results[i],
|
|
1250
1480
|
chargedCount: acceptedForItems,
|
|
@@ -1252,7 +1482,7 @@ async function pushDataAndCharge({ chargingManager, items, eventName, isDefaultD
|
|
|
1252
1482
|
};
|
|
1253
1483
|
}
|
|
1254
1484
|
}
|
|
1255
|
-
// Report rows charged
|
|
1485
|
+
// Report rows charged for the pushed event: summing the
|
|
1256
1486
|
// explicit and default-dataset events would count every row twice.
|
|
1257
1487
|
chargeResult = { ...results.reduce(mergeChargeResults), chargedCount: acceptedForItems };
|
|
1258
1488
|
if (isDefaultDataset && acceptedForItems < limitedItems.length) {
|
|
@@ -1263,15 +1493,19 @@ async function pushDataAndCharge({ chargingManager, items, eventName, isDefaultD
|
|
|
1263
1493
|
// zero): undo every platform charge this operation made, so no
|
|
1264
1494
|
// result is billed for items that are never saved.
|
|
1265
1495
|
for (let i = 0; i < reservedInOp.length; i++) {
|
|
1266
|
-
const
|
|
1496
|
+
const entry = reservedInOp[i];
|
|
1497
|
+
const { name, operationId, uncertain } = entry;
|
|
1498
|
+
let { reserved } = entry;
|
|
1499
|
+
if (entry.credit) {
|
|
1500
|
+
returnVoidCredit(chargingManager, name, entry.credit);
|
|
1501
|
+
reserved -= entry.credit;
|
|
1502
|
+
entry.credit = 0;
|
|
1503
|
+
}
|
|
1267
1504
|
if (reserved <= 0 || !operationId)
|
|
1268
1505
|
continue;
|
|
1269
1506
|
// Original count: the local reservation is released only when
|
|
1270
1507
|
// the platform confirms the void (or that it never applied).
|
|
1271
|
-
|
|
1272
|
-
await chargingManager._cancelUncertainCharge?.(name, operationId, reserved);
|
|
1273
|
-
else
|
|
1274
|
-
await chargingManager._voidCharge(name, operationId, reserved);
|
|
1508
|
+
await voidOrCredit(chargingManager, name, operationId, reserved, !!uncertain);
|
|
1275
1509
|
}
|
|
1276
1510
|
return {
|
|
1277
1511
|
eventChargeLimitReached: true,
|
|
@@ -1297,16 +1531,21 @@ async function pushDataAndCharge({ chargingManager, items, eventName, isDefaultD
|
|
|
1297
1531
|
if (!pushCertainlyFailed(pushErr))
|
|
1298
1532
|
throw pushErr;
|
|
1299
1533
|
for (let i = 0; i < reservedInOp.length; i++) {
|
|
1300
|
-
const
|
|
1301
|
-
const
|
|
1534
|
+
const entry = reservedInOp[i];
|
|
1535
|
+
const { name, operationId, uncertain } = entry;
|
|
1536
|
+
let reserved = results[i].chargedCount;
|
|
1537
|
+
if (entry.credit) {
|
|
1538
|
+
const back = Math.min(reserved, entry.credit);
|
|
1539
|
+
returnVoidCredit(chargingManager, name, back);
|
|
1540
|
+
entry.credit -= back;
|
|
1541
|
+
reserved -= back;
|
|
1542
|
+
}
|
|
1302
1543
|
if (reserved <= 0)
|
|
1303
1544
|
continue;
|
|
1304
|
-
if (operationId
|
|
1305
|
-
//
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
else if (operationId) {
|
|
1309
|
-
await chargingManager._voidCharge(name, operationId, reserved);
|
|
1545
|
+
if (operationId) {
|
|
1546
|
+
// An unconfirmed charge is cancelled (never re-posted for items
|
|
1547
|
+
// that were not stored); a refused void becomes credit.
|
|
1548
|
+
await voidOrCredit(chargingManager, name, operationId, reserved, !!uncertain);
|
|
1310
1549
|
}
|
|
1311
1550
|
else if (isSyntheticEvent(name)) {
|
|
1312
1551
|
chargingManager.applyAcceptedCount(name, reserved, 0);
|
|
@@ -1351,7 +1590,7 @@ function validateRowCharges(charges) {
|
|
|
1351
1590
|
* the rows that are kept, so no row is stored part-paid and no event is billed
|
|
1352
1591
|
* for a row that was not stored. Unpriced events are ignored.
|
|
1353
1592
|
*/
|
|
1354
|
-
async function pushDataWithCharges({ chargingManager, items, charges, isDefaultDataset, pushFn, chargePer = 'row', }) {
|
|
1593
|
+
async function pushDataWithCharges({ chargingManager, items, charges, isDefaultDataset, pushFn, chargePer = 'row', validateItems, }) {
|
|
1355
1594
|
const perRowIn = validateRowCharges(charges);
|
|
1356
1595
|
if (chargePer !== 'row' && chargePer !== 'batch')
|
|
1357
1596
|
throw new Error("chargePer must be 'row' or 'batch'");
|
|
@@ -1384,16 +1623,20 @@ async function pushDataWithCharges({ chargingManager, items, charges, isDefaultD
|
|
|
1384
1623
|
if (!voidsConfirmed && Object.keys(perRow).some((n) => !isSyntheticEvent(n))) {
|
|
1385
1624
|
throw new Error('[Scrapely] An earlier charge for a failed dataset push could not be voided yet; retry the push');
|
|
1386
1625
|
}
|
|
1626
|
+
// An item the storage would refuse fails here, before it is charged.
|
|
1627
|
+
assertItemsSerializable(rowsIn);
|
|
1628
|
+
validateItems?.(rowsIn);
|
|
1387
1629
|
const undo = async (ops) => {
|
|
1388
1630
|
for (const op of ops) {
|
|
1389
1631
|
if (op.charged <= 0)
|
|
1390
1632
|
continue;
|
|
1391
|
-
if (
|
|
1633
|
+
if (op.credit)
|
|
1634
|
+
returnVoidCredit(chargingManager, op.name, op.charged);
|
|
1635
|
+
else if (!op.operationId)
|
|
1392
1636
|
chargingManager.applyAcceptedCount(op.name, op.charged, 0);
|
|
1393
|
-
|
|
1394
|
-
await chargingManager._cancelUncertainCharge(op.name, op.operationId, op.charged);
|
|
1637
|
+
// Cancel / void; a refused void stays billed and becomes credit.
|
|
1395
1638
|
else
|
|
1396
|
-
await chargingManager
|
|
1639
|
+
await voidOrCredit(chargingManager, op.name, op.operationId, op.charged, op.uncertain);
|
|
1397
1640
|
}
|
|
1398
1641
|
};
|
|
1399
1642
|
const order = Object.entries(perRow).sort(([a], [b]) => Number(isSyntheticEvent(a)) - Number(isSyntheticEvent(b)) || a.localeCompare(b));
|
|
@@ -1405,19 +1648,25 @@ async function pushDataWithCharges({ chargingManager, items, charges, isDefaultD
|
|
|
1405
1648
|
const want = n * rows;
|
|
1406
1649
|
chargingManager._takeLastAcceptedOperationId?.();
|
|
1407
1650
|
let got;
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
ops.push({ name, charged: got, operationId: chargingManager._takeLastAcceptedOperationId?.(), uncertain: false });
|
|
1651
|
+
if (takeVoidCredit(chargingManager, name, want) > 0) {
|
|
1652
|
+
got = want;
|
|
1653
|
+
ops.push({ name, charged: want, uncertain: false, credit: true });
|
|
1412
1654
|
}
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
await
|
|
1416
|
-
|
|
1655
|
+
else {
|
|
1656
|
+
try {
|
|
1657
|
+
const r = await chargingManager._chargeExclusive({ eventName: name, count: want });
|
|
1658
|
+
got = r.chargedCount;
|
|
1659
|
+
ops.push({ name, charged: got, operationId: chargingManager._takeLastAcceptedOperationId?.(), uncertain: false });
|
|
1660
|
+
}
|
|
1661
|
+
catch (err) {
|
|
1662
|
+
if (!(err instanceof ChargeUncertainError)) {
|
|
1663
|
+
await undo(ops);
|
|
1664
|
+
throw err;
|
|
1665
|
+
}
|
|
1666
|
+
// Not confirmed: stays pending and is re-posted at exit, so it counts as accepted.
|
|
1667
|
+
got = err.count;
|
|
1668
|
+
ops.push({ name, charged: got, operationId: err.operationId, uncertain: true });
|
|
1417
1669
|
}
|
|
1418
|
-
// Not confirmed: stays pending and is re-posted at exit, so it counts as accepted.
|
|
1419
|
-
got = err.count;
|
|
1420
|
-
ops.push({ name, charged: got, operationId: err.operationId, uncertain: true });
|
|
1421
1670
|
}
|
|
1422
1671
|
if (got < want) {
|
|
1423
1672
|
shortTo = Math.floor(got / n);
|
package/dist/configuration.js
CHANGED
|
@@ -86,8 +86,8 @@ class Configuration extends core_1.Configuration {
|
|
|
86
86
|
defaultDatasetId: 'default',
|
|
87
87
|
defaultRequestQueueId: 'default',
|
|
88
88
|
inputKey: 'INPUT',
|
|
89
|
-
apiBaseUrl: 'https://api
|
|
90
|
-
apiPublicBaseUrl: 'https://api
|
|
89
|
+
apiBaseUrl: 'https://api.scrape.ly',
|
|
90
|
+
apiPublicBaseUrl: 'https://api.scrape.ly',
|
|
91
91
|
proxyStatusUrl: 'http://proxy.scrape.ly',
|
|
92
92
|
proxyHostname: 'proxy.scrape.ly',
|
|
93
93
|
proxyPort: 8000,
|
package/dist/index.js
CHANGED
|
@@ -90,7 +90,7 @@ Object.defineProperty(exports, "MissingRouteError", { enumerable: true, get: fun
|
|
|
90
90
|
Object.defineProperty(exports, "CriticalError", { enumerable: true, get: function () { return core_2.CriticalError; } });
|
|
91
91
|
Object.defineProperty(exports, "useState", { enumerable: true, get: function () { return core_2.useState; } });
|
|
92
92
|
Object.defineProperty(exports, "mergeCookies", { enumerable: true, get: function () { return core_2.mergeCookies; } });
|
|
93
|
-
//
|
|
93
|
+
// v2-style utils namespace
|
|
94
94
|
var utils_js_1 = require("./utils.js");
|
|
95
95
|
Object.defineProperty(exports, "utils", { enumerable: true, get: function () { return utils_js_1.utils; } });
|
|
96
96
|
Object.defineProperty(exports, "enqueueLinks", { enumerable: true, get: function () { return utils_js_1.enqueueLinks; } });
|
package/dist/node-wrapper.js
CHANGED
|
@@ -57,7 +57,7 @@ const SCRIPT_CONFIG_ENV_RE = new RegExp(`^npm_config_(${[...SCRIPT_CONFIG_KEYS,
|
|
|
57
57
|
/**
|
|
58
58
|
* Whether an npmrc file sets any of SCRIPT_CONFIG_KEYS. Other settings
|
|
59
59
|
* (progress=false, loglevel, registry, auth, ...) don't affect running a
|
|
60
|
-
* script —
|
|
60
|
+
* script — the standard actor Dockerfiles write `progress=false` to ~/.npmrc.
|
|
61
61
|
* Unreadable or unparseable files count as setting them.
|
|
62
62
|
*/
|
|
63
63
|
function readNpmrc(file) {
|
|
@@ -1033,8 +1033,12 @@ function installEsmPuppeteerHook() {
|
|
|
1033
1033
|
return;
|
|
1034
1034
|
Module._scrapelyEsmPuppeteerHook = true;
|
|
1035
1035
|
try {
|
|
1036
|
-
|
|
1037
|
-
|
|
1036
|
+
// Node 22's synchronous hooks break require(esm) of CommonJS graphs that
|
|
1037
|
+
// ESM imports (crawlee -> linkedom: "request for 'boolbase' is not in cache");
|
|
1038
|
+
// Node 22 uses the off-thread loader instead.
|
|
1039
|
+
const nodeMajor = Number(process.versions.node.split('.')[0]);
|
|
1040
|
+
if (typeof Module.registerHooks === 'function' && nodeMajor >= 24) {
|
|
1041
|
+
// Node 24+: synchronous, in-thread.
|
|
1038
1042
|
Module.registerHooks({
|
|
1039
1043
|
load(url, context, nextLoad) {
|
|
1040
1044
|
const result = nextLoad(url, context);
|
|
@@ -1047,7 +1051,7 @@ function installEsmPuppeteerHook() {
|
|
|
1047
1051
|
});
|
|
1048
1052
|
}
|
|
1049
1053
|
else if (typeof Module.register === 'function') {
|
|
1050
|
-
// Node 20.6+ / 22
|
|
1054
|
+
// Node 20.6+ / 22: off-thread loader (esm-loader-hooks.mjs).
|
|
1051
1055
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
1052
1056
|
const { pathToFileURL } = require('url');
|
|
1053
1057
|
Module.register('./esm-loader-hooks.mjs', pathToFileURL(__filename));
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { ProxyConfigurationOptions as CoreProxyConfigurationOptions, ProxyInfo as CoreProxyInfo } from '@crawlee/core';
|
|
2
2
|
import { ProxyConfiguration } from './scrapely-proxy-configuration.js';
|
|
3
3
|
/**
|
|
4
|
-
* Proxy info enriched with the platform-proxy
|
|
5
|
-
*
|
|
4
|
+
* Proxy info enriched with the platform-proxy username fields
|
|
5
|
+
* (extends Crawlee's `ProxyInfo` type).
|
|
6
6
|
*/
|
|
7
7
|
export interface ProxyInfo extends CoreProxyInfo {
|
|
8
8
|
/** Proxy groups used by the platform proxy (e.g. `['RESIDENTIAL']`). */
|
|
@@ -18,33 +18,33 @@ export interface ProxyInfo extends CoreProxyInfo {
|
|
|
18
18
|
* Modes supported:
|
|
19
19
|
* - Platform proxy (default on Scrapely runs): when the server injects
|
|
20
20
|
* APIFY_PROXY_* or SCRAPELY_PROXY_* credentials, groups/countryCode/password
|
|
21
|
-
* build
|
|
21
|
+
* build gateway usernames automatically.
|
|
22
22
|
* - Bring-your-own-proxy (BYOP): custom proxyUrls / newUrlFunction /
|
|
23
23
|
* tieredProxyUrls always win over the platform proxy.
|
|
24
24
|
* - Mixed: the 'auto' (or 'http://auto') sentinel inside proxyUrls /
|
|
25
25
|
* tieredProxyUrls / tieredProxyConfig[].proxyUrls reserves a slot for the
|
|
26
|
-
* platform proxy alongside BYOP entries
|
|
26
|
+
* platform proxy alongside BYOP entries.
|
|
27
27
|
*/
|
|
28
28
|
export interface ProxyConfigurationOptions extends CoreProxyConfigurationOptions {
|
|
29
29
|
/**
|
|
30
|
-
* Multiple different ProxyConfigurationOptions stratified into tiers
|
|
30
|
+
* Multiple different ProxyConfigurationOptions stratified into tiers.
|
|
31
31
|
* Tiers with `proxyUrls` map directly to tieredProxyUrls; tiers without
|
|
32
32
|
* URLs expand their platform options (groups/countryCode/password) to a
|
|
33
|
-
* concrete platform proxy URL
|
|
33
|
+
* concrete platform proxy URL.
|
|
34
34
|
*/
|
|
35
35
|
tieredProxyConfig?: Omit<ProxyConfigurationOptions, 'tieredProxyConfig' | 'tieredProxyUrls'>[];
|
|
36
36
|
/**
|
|
37
|
-
* Platform proxy groups
|
|
37
|
+
* Platform proxy groups. Effective in platform proxy
|
|
38
38
|
* mode; ignored for BYOP configurations.
|
|
39
39
|
*/
|
|
40
40
|
groups?: string[];
|
|
41
41
|
/**
|
|
42
|
-
* Platform proxy country code
|
|
42
|
+
* Platform proxy country code. Effective in platform
|
|
43
43
|
* proxy mode; ignored for BYOP configurations.
|
|
44
44
|
*/
|
|
45
45
|
countryCode?: string;
|
|
46
46
|
/**
|
|
47
|
-
* Platform proxy subdivision code, e.g. US state
|
|
47
|
+
* Platform proxy subdivision code, e.g. US state.
|
|
48
48
|
* Requires `countryCode`. Effective in platform proxy mode; ignored for
|
|
49
49
|
* BYOP configurations. Emitted as a merged `country-XX_YY` username
|
|
50
50
|
* fragment (ISO 3166-2).
|
|
@@ -90,7 +90,7 @@ export interface CreateProxyConfigurationOptions extends ProxyConfigurationOptio
|
|
|
90
90
|
/**
|
|
91
91
|
* Platform proxy connection details injected into run containers by the
|
|
92
92
|
* Scrapely server (backed by Evomi). SCRAPELY_* vars take precedence over the
|
|
93
|
-
*
|
|
93
|
+
* APIFY_* aliases so Scrapely-first deployments stay unambiguous.
|
|
94
94
|
*/
|
|
95
95
|
export interface PlatformProxyEnv {
|
|
96
96
|
hostname: string;
|
|
@@ -114,7 +114,7 @@ export declare function getPlatformProxyEnv(): PlatformProxyEnv | null;
|
|
|
114
114
|
* Creates a proxy configuration for use with crawlers.
|
|
115
115
|
*
|
|
116
116
|
* The platform proxy (Evomi-backed) is injected into runs via the SCRAPELY_PROXY
|
|
117
|
-
* and APIFY_PROXY environment variables.
|
|
117
|
+
* and APIFY_PROXY environment variables. Platform proxy options (groups, countryCode, ...)
|
|
118
118
|
* build gateway usernames in platform mode; proxyUrls, newUrlFunction and
|
|
119
119
|
* tieredProxyUrls configure bring-your-own proxies instead.
|
|
120
120
|
*/
|
|
@@ -57,7 +57,7 @@ function normalizeProxyInput(options) {
|
|
|
57
57
|
* Creates a proxy configuration for use with crawlers.
|
|
58
58
|
*
|
|
59
59
|
* The platform proxy (Evomi-backed) is injected into runs via the SCRAPELY_PROXY
|
|
60
|
-
* and APIFY_PROXY environment variables.
|
|
60
|
+
* and APIFY_PROXY environment variables. Platform proxy options (groups, countryCode, ...)
|
|
61
61
|
* build gateway usernames in platform mode; proxyUrls, newUrlFunction and
|
|
62
62
|
* tieredProxyUrls configure bring-your-own proxies instead.
|
|
63
63
|
*/
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Proxy input objects with both namings. An input-schema proxy field may be
|
|
3
3
|
* written with the Scrapely names (`useScrapelyProxy`, `scrapelyProxyGroups`,
|
|
4
|
-
* `scrapelyProxyCountry`, `scrapelyProxySubdivision`) or the
|
|
4
|
+
* `scrapelyProxyCountry`, `scrapelyProxySubdivision`) or the legacy names
|
|
5
5
|
* (`useApifyProxy`, `apifyProxy*`). Actor.getInput() and Actor.input return
|
|
6
6
|
* every set option under both names, so actor code that reads either naming
|
|
7
7
|
* sees the user's choice.
|