node-autotask 0.18.0 → 0.20.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.
- package/ARCHITECTURE.md +15 -8
- package/CHANGELOG.md +590 -0
- package/MCP_TOOL_CATALOG.json +6 -6
- package/MCP_TOOL_MANIFEST.md +128 -128
- package/N8N_MCP_MANIFEST.json +44 -9
- package/N8N_MCP_MANIFEST.md +8 -8
- package/README.md +8 -4
- package/capabilities.json +1941 -1829
- package/capabilities.schema.json +6 -0
- package/dist/capabilities.cjs +2 -2
- package/dist/capabilities.js +1 -1
- package/dist/{chunk-VV2UUW2J.js → chunk-2T77J2ZM.js} +1940 -1828
- package/dist/{chunk-5O5EPYBO.cjs → chunk-FLYTKJKV.cjs} +1940 -1828
- package/dist/{chunk-VYUZZIEE.js → chunk-GA6UCX3Z.js} +292 -58
- package/dist/{chunk-W2WSZOZA.js → chunk-JEJUFXHX.js} +2 -2
- package/dist/{chunk-WRWX6WIK.cjs → chunk-L6V66V22.cjs} +360 -126
- package/dist/{chunk-G564CHXM.cjs → chunk-RMLGOCGA.cjs} +135 -135
- package/dist/{chunk-HXOWEYV3.js → chunk-XSYJB4IW.js} +3 -3
- package/dist/{chunk-7I3F3AJJ.cjs → chunk-YH5CPRDN.cjs} +394 -394
- package/dist/{index-D7x3MkLK.d.cts → index-AN-NjLi9.d.cts} +126 -21
- package/dist/{index-BqPY04ZG.d.ts → index-CvLLsOCP.d.ts} +126 -21
- package/dist/index.cjs +24 -22
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +6 -4
- package/dist/operations/index.cjs +184 -136
- package/dist/operations/index.d.cts +2 -2
- package/dist/operations/index.d.ts +2 -2
- package/dist/operations/index.js +79 -31
- package/dist/pool.cjs +8 -8
- package/dist/pool.d.cts +1 -1
- package/dist/pool.d.ts +1 -1
- package/dist/pool.js +4 -4
- package/dist/resources/index.cjs +4 -4
- package/dist/resources/index.d.cts +1 -1
- package/dist/resources/index.d.ts +1 -1
- package/dist/resources/index.js +3 -3
- package/docs/API.md +126 -40
- package/package.json +2 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
CAPABILITIES
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-2T77J2ZM.js";
|
|
4
4
|
|
|
5
5
|
// src/auth.ts
|
|
6
6
|
var AUTH_HEADERS = {
|
|
@@ -1536,6 +1536,7 @@ var DEFAULT_TIMEOUT_MS = 3e4;
|
|
|
1536
1536
|
var DEFAULT_MAX_RETRIES = 3;
|
|
1537
1537
|
var DEFAULT_MAINTENANCE_MAX_WAIT_MS = 36e5;
|
|
1538
1538
|
var DEFAULT_MAINTENANCE_POLL_MS = 3e4;
|
|
1539
|
+
var MAINTENANCE_WINDOW_MAX_MS = 4 * 36e5;
|
|
1539
1540
|
var DEFAULT_MAX_CONCURRENT = 10;
|
|
1540
1541
|
var DEFAULT_MAX_RESPONSE_BYTES = 64 * 1024 * 1024;
|
|
1541
1542
|
var DEFAULT_MAX_SCAN_RECORDS = 500;
|
|
@@ -2073,6 +2074,88 @@ function refusedFilterShape(filter) {
|
|
|
2073
2074
|
// src/http.ts
|
|
2074
2075
|
import { AsyncLocalStorage } from "async_hooks";
|
|
2075
2076
|
|
|
2077
|
+
// src/latest-count-cache.ts
|
|
2078
|
+
var LATEST_COUNT_CACHE_TTL_MS = 3e4;
|
|
2079
|
+
var LATEST_COUNT_CACHE_MAX_ENTRIES = 4096;
|
|
2080
|
+
var LatestCountCache = class {
|
|
2081
|
+
/** The TTL in ms. */
|
|
2082
|
+
ttlMs;
|
|
2083
|
+
/** The entry bound (applies to STORED entries). */
|
|
2084
|
+
maxEntries;
|
|
2085
|
+
store = /* @__PURE__ */ new Map();
|
|
2086
|
+
/** The invalidation epoch (the `ReadCache` pattern — issue #139): bumped
|
|
2087
|
+
* by `clear()`; capture it before a wire read and pass it to `set` so a
|
|
2088
|
+
* probe in flight across a write cannot re-store its stale answer. */
|
|
2089
|
+
epochCounter = 0;
|
|
2090
|
+
constructor(ttlMs, maxEntries = LATEST_COUNT_CACHE_MAX_ENTRIES) {
|
|
2091
|
+
if (!Number.isInteger(ttlMs) || ttlMs <= 0) {
|
|
2092
|
+
throw new AutotaskConfigError(`latestCountCache.ttlMs must be a positive integer (got ${String(ttlMs)})`);
|
|
2093
|
+
}
|
|
2094
|
+
if (!Number.isInteger(maxEntries) || maxEntries <= 0) {
|
|
2095
|
+
throw new AutotaskConfigError(`latestCountCache.maxEntries must be a positive integer (got ${String(maxEntries)})`);
|
|
2096
|
+
}
|
|
2097
|
+
this.ttlMs = ttlMs;
|
|
2098
|
+
this.maxEntries = maxEntries;
|
|
2099
|
+
}
|
|
2100
|
+
/** The composite key: (resource plan key, canonical filter serialisation)
|
|
2101
|
+
* — the `\u0000` separator is unencodable in a filter value, so no two
|
|
2102
|
+
* (resource, conditions) pairs collide. */
|
|
2103
|
+
static key(resourceKey, conditions) {
|
|
2104
|
+
return `${resourceKey}\0${JSON.stringify(conditions ?? [])}`;
|
|
2105
|
+
}
|
|
2106
|
+
/** The live entry's count, or `undefined` (absent OR expired — an expired
|
|
2107
|
+
* entry is purged lazily on this read). */
|
|
2108
|
+
get(key) {
|
|
2109
|
+
const entry = this.store.get(key);
|
|
2110
|
+
if (entry === void 0) return void 0;
|
|
2111
|
+
if (entry.expiresAt <= Date.now()) {
|
|
2112
|
+
this.store.delete(key);
|
|
2113
|
+
return void 0;
|
|
2114
|
+
}
|
|
2115
|
+
return entry.value;
|
|
2116
|
+
}
|
|
2117
|
+
/** The invalidation epoch: bumped by `clear()`; capture it before a wire
|
|
2118
|
+
* read and pass it to `set` so a probe in flight across a write cannot
|
|
2119
|
+
* re-store its stale answer (issue #139, the `ReadCache` pattern). */
|
|
2120
|
+
get epoch() {
|
|
2121
|
+
return this.epochCounter;
|
|
2122
|
+
}
|
|
2123
|
+
/** Store a SUCCESSFUL plain-number count under `key`, live for `ttlMs`.
|
|
2124
|
+
* `epoch` is the invalidation epoch the caller captured BEFORE its wire
|
|
2125
|
+
* read: a probe that straddled a `clear()` (a write) is dropped, never
|
|
2126
|
+
* re-stored with its pre-write body (issue #139). Past the cap: purge
|
|
2127
|
+
* expired entries first, then evict the oldest-inserted (Map iteration
|
|
2128
|
+
* order = insertion order). */
|
|
2129
|
+
set(key, value, epoch = this.epochCounter) {
|
|
2130
|
+
if (epoch !== this.epochCounter) return;
|
|
2131
|
+
if (!Number.isFinite(value)) return;
|
|
2132
|
+
const entry = { value, expiresAt: Date.now() + this.ttlMs };
|
|
2133
|
+
if (!this.store.has(key) && this.store.size >= this.maxEntries) {
|
|
2134
|
+
for (const [k, e] of this.store) {
|
|
2135
|
+
if (e.expiresAt <= Date.now()) this.store.delete(k);
|
|
2136
|
+
}
|
|
2137
|
+
if (this.store.size >= this.maxEntries) {
|
|
2138
|
+
const oldest = this.store.keys().next();
|
|
2139
|
+
if (!oldest.done) this.store.delete(oldest.value);
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
this.store.set(key, entry);
|
|
2143
|
+
}
|
|
2144
|
+
/** Drop one entry (absent = no-op). Issue #157: the stale-LOW recovery
|
|
2145
|
+
* deletes the cached total before its one fresh wire re-probe, so the
|
|
2146
|
+
* re-probe cannot be served by the entry it is meant to replace. */
|
|
2147
|
+
delete(key) {
|
|
2148
|
+
this.store.delete(key);
|
|
2149
|
+
}
|
|
2150
|
+
/** Clear every entry (a real write through the owning client — the
|
|
2151
|
+
* write-then-read re-fetch rule) and bump the invalidation epoch: a
|
|
2152
|
+
* `set` captured before this `clear` is discarded (issue #139). */
|
|
2153
|
+
clear() {
|
|
2154
|
+
this.epochCounter += 1;
|
|
2155
|
+
this.store.clear();
|
|
2156
|
+
}
|
|
2157
|
+
};
|
|
2158
|
+
|
|
2076
2159
|
// src/relationships.ts
|
|
2077
2160
|
var RELATIONSHIP_ZONE = {
|
|
2078
2161
|
"zone": "ww29",
|
|
@@ -8305,7 +8388,7 @@ var RELATIONSHIP_MAP = {
|
|
|
8305
8388
|
],
|
|
8306
8389
|
"naturalKeys": {
|
|
8307
8390
|
"hasSearch": true,
|
|
8308
|
-
"searchCovers": "title only (contains); ticketNumber lookups use resolve / findByTicketNumber",
|
|
8391
|
+
"searchCovers": "title only (contains \u2014 the vendor's contains match is looser than a literal substring (case/token matching): a search can return MORE hits than a local title-substring check (measured 2026-10-19: 25 vs 24); a per-field contains filter in tickets.query is the precise tool); ticketNumber lookups use resolve / findByTicketNumber",
|
|
8309
8392
|
"findBy": [
|
|
8310
8393
|
"findByTicketNumber"
|
|
8311
8394
|
]
|
|
@@ -10252,8 +10335,8 @@ var EMPTY_REASON_STAMPS = {
|
|
|
10252
10335
|
var POINTER_NOTES = {
|
|
10253
10336
|
"ticket_history.query": `W1 (ww29 (australia-nz) live 2026-10-04; vendor contract change live-verified 2026-10-06): the vendor requires ticket_history.query to be supplied with exactly ONE ticketID-equals filter \u2014 an unfiltered call 500s by contract (10-06 text: "When querying the TicketHistory entity you are required to supply one and only one ticketID equals filter"). WITH the filter the query WORKS (live 2026-10-06: rows returned, hasMore true \u2014 a single {op:'eq', field:'ticketID', value:<id>} condition; the filter vocabulary is eq/noteq/gt/gte/lt/lte/beginsWith/endsWith/contains/exist/notExist/in/notIn/and/or). tickets.timeline / tickets.fullContext remain the convenient composites; raw history rows come from the filtered query.`,
|
|
10254
10337
|
"time_entries.create": `W-A3 (ww29 (australia-nz) live 2026-10-04; drift re-verified 2026-10-06): the vendor now refuses ALL time-entry writes for this API-only user with "API-only user cannot be selected for this field" (10-04 text: "The specified AssignedResourceID and AssignedRoleID combination is not currently defined"). UNSATISFIABLE for an API-only integration user: the resources table is blind (0 rows) and the only selectable resource is the user's own API-only record, which the vendor refuses. createForResource \u2014 stage-11 PROVEN (8 live creates, 10-04) \u2014 is walled by the SAME drift (confirm-100 receipt 2026-10-06). Tenant evidence that valid pairs exist: postedWithin rows (resourceID 31131373 / 29869529 with roleID 29682834) \u2014 human resources the API user cannot select.`,
|
|
10255
|
-
"time_entries.getPosted":
|
|
10256
|
-
"time_entries.getUnposted":
|
|
10338
|
+
"time_entries.getPosted": "W-D3 (ww29 (australia-nz) live 2026-10-04, re-verified live 2026-10-15): this zone's TimeEntry entity exposes NO posted-state filter field \u2014 the vendor rejects postingStatus, postedDate and status with \"Unable to find X in the TimeEntry Entity\" (live field-existence oracle 2026-10-04: only dateWorked / resourceID / ticketID / taskID resolve as filter fields). Re-probed 2026-10-15 (ww29 re-probe campaign): time_entries.getPosted live 500, vendor body verbatim 'Unable to find postedDate in the TimeEntry Entity.' \u2014 a vendor endpoint defect on AU03, not a transient fault: posted cannot be filtered server-side on this zone \u2014 use time_entries.postedWithin (the dateWorked window, live-verified working \u2014 omit the `posted` narrowing on this zone: it sends postedDate, which the zone 500s) or read time_entries.query and derive posted-state from the fields your zone's entity exposes",
|
|
10339
|
+
"time_entries.getUnposted": "W-D3 (ww29 (australia-nz) live 2026-10-04, re-verified live 2026-10-15): this zone's TimeEntry entity exposes NO posted-state filter field \u2014 the vendor rejects postingStatus, postedDate and status with \"Unable to find X in the TimeEntry Entity\" (live field-existence oracle 2026-10-04: only dateWorked / resourceID / ticketID / taskID resolve as filter fields). Re-probed 2026-10-15 (ww29 re-probe campaign): time_entries.getUnposted live 500, vendor body verbatim 'Unable to find postedDate in the TimeEntry Entity.' \u2014 a vendor endpoint defect on AU03, not a transient fault: unposted cannot be filtered server-side on this zone \u2014 use time_entries.postedWithin (the dateWorked window, live-verified working \u2014 omit the `posted` narrowing on this zone: it sends postedDate, which the zone 500s) or read time_entries.query and derive posted-state from the fields your zone's entity exposes",
|
|
10257
10340
|
"authenticate.get": "W2: authenticate.get is DESIGNATOR-ONLY \u2014 it returns the designator identity, not a tenant data surface; a miss/empty here is not a data-empty signal (no emptyReason is attached by design \u2014 a false reason would be worse than none)"
|
|
10258
10341
|
};
|
|
10259
10342
|
var BLIND_SURFACE_INVENTORY = {
|
|
@@ -14779,9 +14862,21 @@ function backoffDelay(attempt) {
|
|
|
14779
14862
|
const exp = Math.min(BACKOFF_CAP_MS, BACKOFF_BASE_MS * 2 ** attempt);
|
|
14780
14863
|
return Math.floor(exp / 2 + Math.random() * (exp / 2));
|
|
14781
14864
|
}
|
|
14782
|
-
|
|
14783
|
-
|
|
14784
|
-
return
|
|
14865
|
+
var MAINTENANCE_PROBE_BYTES = 256;
|
|
14866
|
+
function maintenanceProbe(text) {
|
|
14867
|
+
return Buffer.from(text, "utf8").subarray(0, MAINTENANCE_PROBE_BYTES).toString("utf8");
|
|
14868
|
+
}
|
|
14869
|
+
function looksLikeMaintenanceText(text) {
|
|
14870
|
+
return /maintenance|temporarily unavailable|service unavailable|outage|downtime|down for/i.test(
|
|
14871
|
+
maintenanceProbe(text)
|
|
14872
|
+
);
|
|
14873
|
+
}
|
|
14874
|
+
function isHtmlLike(contentType, body) {
|
|
14875
|
+
if (contentType !== null && /text\/html|application\/xhtml/i.test(contentType)) return true;
|
|
14876
|
+
return body.trimStart().startsWith("<");
|
|
14877
|
+
}
|
|
14878
|
+
function maintenanceExhaustedAction(maxWaitMs, waits) {
|
|
14879
|
+
return `The zone stayed in a maintenance window for ${Math.round(maxWaitMs / 6e4)} min \u2014 the SDK waited and retried ${waits} time(s) and gave up. Re-run after the window (the observed median is about 2 h; the ceiling is 4 h \u2014 MAINTENANCE_WINDOW_MAX_MS), or raise maintenanceMaxWaitMs.`;
|
|
14785
14880
|
}
|
|
14786
14881
|
function parseRetryAfter(value) {
|
|
14787
14882
|
if (value === null || value.length === 0) return void 0;
|
|
@@ -14875,6 +14970,11 @@ var HttpClient = class {
|
|
|
14875
14970
|
fieldCache;
|
|
14876
14971
|
/** Resource-name cache for `operations.expandResourceNames` (issue #80: the default instance when `resourceNameCache` is absent; undefined only when the user set `false` explicitly, issue #65). */
|
|
14877
14972
|
resourceNameCache;
|
|
14973
|
+
/** The `latest` walk's count-probe cache (issue #127: the short-TTL
|
|
14974
|
+
* per-client count cache — ALWAYS on, process-internal: no config
|
|
14975
|
+
* surface, no served keys; the plain `count` op and `operations.aggregate`
|
|
14976
|
+
* bypass it by construction and promise fresh totals). */
|
|
14977
|
+
latestCountCache;
|
|
14878
14978
|
/** Resolved update fallback gate (issue #34; OFF by default). */
|
|
14879
14979
|
update;
|
|
14880
14980
|
auth;
|
|
@@ -14940,6 +15040,7 @@ var HttpClient = class {
|
|
|
14940
15040
|
this.readCache = config.readCache;
|
|
14941
15041
|
this.fieldCache = config.fieldCache;
|
|
14942
15042
|
this.resourceNameCache = config.resourceNameCache;
|
|
15043
|
+
this.latestCountCache = new LatestCountCache(LATEST_COUNT_CACHE_TTL_MS);
|
|
14943
15044
|
this.update = config.update;
|
|
14944
15045
|
this.zone = config.zone;
|
|
14945
15046
|
this.reResolveZoneOn401 = config.reResolveZoneOn401;
|
|
@@ -15217,6 +15318,7 @@ var HttpClient = class {
|
|
|
15217
15318
|
const maintenancePollMs = opts.maintenancePollMs ?? this.maintenancePollMs;
|
|
15218
15319
|
let firstMaintenanceAt;
|
|
15219
15320
|
let maintenanceWaits = 0;
|
|
15321
|
+
let maintenanceBudgetSpent = false;
|
|
15220
15322
|
const started = Date.now();
|
|
15221
15323
|
let deadlineAt = started + timeoutMs;
|
|
15222
15324
|
const callerSignal = opts.signal ?? ambientSignal.getStore();
|
|
@@ -15437,12 +15539,13 @@ var HttpClient = class {
|
|
|
15437
15539
|
httpStatus: status,
|
|
15438
15540
|
suggestedAction: "The zone returned a non-JSON 2xx body; verify baseUrl points at the REST API."
|
|
15439
15541
|
});
|
|
15440
|
-
if (err2.code === "UNEXPECTED_CONTENT_TYPE" && maintenanceMaxWaitMs > 0 &&
|
|
15542
|
+
if (err2.code === "UNEXPECTED_CONTENT_TYPE" && maintenanceMaxWaitMs > 0 && looksLikeMaintenanceText(text)) {
|
|
15441
15543
|
const now = Date.now();
|
|
15442
15544
|
firstMaintenanceAt ??= now;
|
|
15443
15545
|
const remaining = maintenanceMaxWaitMs - (now - firstMaintenanceAt);
|
|
15444
15546
|
if (remaining > 0) {
|
|
15445
15547
|
const wait = Math.min(maintenancePollMs, remaining);
|
|
15548
|
+
if (wait >= remaining) maintenanceBudgetSpent = true;
|
|
15446
15549
|
maintenanceWaits += 1;
|
|
15447
15550
|
this.logger.warn?.(
|
|
15448
15551
|
`[autotask] maintenance window on ${method} ${opts.path}; waiting ${Math.round(wait / 1e3)}s (retry ${maintenanceWaits}, up to ${Math.round(maintenanceMaxWaitMs / 6e4)} min)`,
|
|
@@ -15453,6 +15556,7 @@ var HttpClient = class {
|
|
|
15453
15556
|
await pause(wait);
|
|
15454
15557
|
continue;
|
|
15455
15558
|
}
|
|
15559
|
+
maintenanceBudgetSpent = true;
|
|
15456
15560
|
}
|
|
15457
15561
|
if (err2.code === "UNEXPECTED_CONTENT_TYPE" && retriable5xx && attempt < maxRetries) {
|
|
15458
15562
|
const retryAfter = parseRetryAfter(res.headers.get("Retry-After"));
|
|
@@ -15467,17 +15571,27 @@ var HttpClient = class {
|
|
|
15467
15571
|
continue;
|
|
15468
15572
|
}
|
|
15469
15573
|
}
|
|
15470
|
-
|
|
15574
|
+
const pageSnippet = typeof err2.details?.bodySnippet === "string" ? err2.details.bodySnippet : "";
|
|
15575
|
+
if (maintenanceWaits > 0) {
|
|
15471
15576
|
err2.details = {
|
|
15472
15577
|
...err2.details ?? {},
|
|
15473
15578
|
maintenance: {
|
|
15474
15579
|
waits: maintenanceWaits,
|
|
15475
15580
|
waitedMs: Date.now() - (firstMaintenanceAt ?? Date.now()),
|
|
15476
15581
|
maxWaitMs: maintenanceMaxWaitMs,
|
|
15477
|
-
exhausted:
|
|
15582
|
+
exhausted: maintenanceBudgetSpent
|
|
15478
15583
|
}
|
|
15479
15584
|
};
|
|
15480
|
-
|
|
15585
|
+
if (maintenanceBudgetSpent) {
|
|
15586
|
+
err2.suggestedAction = maintenanceExhaustedAction(maintenanceMaxWaitMs, maintenanceWaits);
|
|
15587
|
+
}
|
|
15588
|
+
}
|
|
15589
|
+
if (err2.code === "UNEXPECTED_CONTENT_TYPE" && !looksLikeMaintenanceText(text) && isHtmlLike(res.headers.get("Content-Type"), text)) {
|
|
15590
|
+
err2.details = { ...err2.details ?? {}, maintenancePatternMiss: true };
|
|
15591
|
+
this.logger.debug?.(
|
|
15592
|
+
`[autotask] non-JSON body did not match the maintenance vocabulary on ${method} ${opts.path}: ${pageSnippet}`,
|
|
15593
|
+
correlationId
|
|
15594
|
+
);
|
|
15481
15595
|
}
|
|
15482
15596
|
attachEmptyReasonDetails(err2);
|
|
15483
15597
|
throw err2;
|
|
@@ -15489,13 +15603,38 @@ var HttpClient = class {
|
|
|
15489
15603
|
break;
|
|
15490
15604
|
}
|
|
15491
15605
|
let body;
|
|
15606
|
+
let nonJsonBody;
|
|
15492
15607
|
if (text.length > 0) {
|
|
15493
15608
|
try {
|
|
15494
15609
|
body = JSON.parse(text);
|
|
15495
15610
|
} catch {
|
|
15496
15611
|
body = text;
|
|
15612
|
+
nonJsonBody = text;
|
|
15497
15613
|
}
|
|
15498
15614
|
}
|
|
15615
|
+
const nonJsonSnippet = nonJsonBody === void 0 ? void 0 : bodySnippet(nonJsonBody, this.redactPersonalData);
|
|
15616
|
+
const nonJsonIsMaintenance = nonJsonBody !== void 0 && looksLikeMaintenanceText(nonJsonBody);
|
|
15617
|
+
const nonJsonIsHtmlCandidate = nonJsonBody !== void 0 && isHtmlLike(res.headers.get("Content-Type"), nonJsonBody);
|
|
15618
|
+
const nonJsonMaintenanceMayWait = nonJsonIsMaintenance && retriable5xx;
|
|
15619
|
+
if (nonJsonBody !== void 0 && maintenanceMaxWaitMs > 0 && nonJsonMaintenanceMayWait) {
|
|
15620
|
+
const now = Date.now();
|
|
15621
|
+
firstMaintenanceAt ??= now;
|
|
15622
|
+
const remaining = maintenanceMaxWaitMs - (now - firstMaintenanceAt);
|
|
15623
|
+
if (remaining > 0) {
|
|
15624
|
+
const wait = Math.min(maintenancePollMs, remaining);
|
|
15625
|
+
if (wait >= remaining) maintenanceBudgetSpent = true;
|
|
15626
|
+
maintenanceWaits += 1;
|
|
15627
|
+
this.logger.warn?.(
|
|
15628
|
+
`[autotask] maintenance window on ${method} ${opts.path} (HTTP ${status}); waiting ${Math.round(wait / 1e3)}s (retry ${maintenanceWaits}, up to ${Math.round(maintenanceMaxWaitMs / 6e4)} min)`,
|
|
15629
|
+
correlationId
|
|
15630
|
+
);
|
|
15631
|
+
deadlineAt = Date.now() + wait + timeoutMs;
|
|
15632
|
+
armDeadline(deadlineAt - Date.now());
|
|
15633
|
+
await pause(wait);
|
|
15634
|
+
continue;
|
|
15635
|
+
}
|
|
15636
|
+
maintenanceBudgetSpent = true;
|
|
15637
|
+
}
|
|
15499
15638
|
const deterministic500 = status >= 500 && isDeterministicVendor500(parseVendorErrors(body));
|
|
15500
15639
|
const retryableStatus = status === 429 || status >= 500;
|
|
15501
15640
|
if (retryableStatus && !deterministic500 && attempt < maxRetries && (status === 429 ? retriable : retriable5xx)) {
|
|
@@ -15537,6 +15676,32 @@ var HttpClient = class {
|
|
|
15537
15676
|
credentialShapeExpectation(headers, this.auth)
|
|
15538
15677
|
);
|
|
15539
15678
|
err.correlationId = correlationId;
|
|
15679
|
+
if (maintenanceWaits > 0) {
|
|
15680
|
+
err.details = {
|
|
15681
|
+
...err.details ?? {},
|
|
15682
|
+
maintenance: {
|
|
15683
|
+
waits: maintenanceWaits,
|
|
15684
|
+
waitedMs: Date.now() - (firstMaintenanceAt ?? Date.now()),
|
|
15685
|
+
maxWaitMs: maintenanceMaxWaitMs,
|
|
15686
|
+
exhausted: maintenanceBudgetSpent
|
|
15687
|
+
}
|
|
15688
|
+
};
|
|
15689
|
+
if (maintenanceBudgetSpent) {
|
|
15690
|
+
err.suggestedAction = maintenanceExhaustedAction(maintenanceMaxWaitMs, maintenanceWaits);
|
|
15691
|
+
}
|
|
15692
|
+
}
|
|
15693
|
+
if (nonJsonBody !== void 0) {
|
|
15694
|
+
if (nonJsonIsMaintenance && !retriable5xx) {
|
|
15695
|
+
err.details = { ...err.details ?? {}, maintenanceDetected: true };
|
|
15696
|
+
err.suggestedAction = `A maintenance-shaped page answered ${method} ${opts.path} (HTTP ${status}); it was NOT waited out or re-sent \u2014 a non-2xx edge page can arrive after the request was forwarded, so verify the record before re-issuing (only a GET or an explicitly idempotent-flagged call waits this window out).`;
|
|
15697
|
+
} else if (!nonJsonIsMaintenance && nonJsonIsHtmlCandidate) {
|
|
15698
|
+
err.details = { ...err.details ?? {}, maintenancePatternMiss: true };
|
|
15699
|
+
this.logger.debug?.(
|
|
15700
|
+
`[autotask] non-JSON body did not match the maintenance vocabulary on ${method} ${opts.path}: ${nonJsonSnippet}`,
|
|
15701
|
+
correlationId
|
|
15702
|
+
);
|
|
15703
|
+
}
|
|
15704
|
+
}
|
|
15540
15705
|
if (status === 401 && zoneRetry !== void 0) {
|
|
15541
15706
|
err.details = {
|
|
15542
15707
|
...err.details ?? {},
|
|
@@ -15609,6 +15774,7 @@ var HttpClient = class {
|
|
|
15609
15774
|
clearTimeout(deadlineTimer);
|
|
15610
15775
|
if (!(method === "GET" || opts.idempotent === true)) {
|
|
15611
15776
|
this.readCache?.clear();
|
|
15777
|
+
this.latestCountCache.clear();
|
|
15612
15778
|
const nameOp = typeof opts.operation === "string" ? opts.operation : void 0;
|
|
15613
15779
|
const nameDot = nameOp !== void 0 ? nameOp.indexOf(".") : -1;
|
|
15614
15780
|
if (this.resourceNameCache !== void 0 && nameOp !== void 0 && nameDot > 0 && nameOp.slice(0, nameDot) === "resources") {
|
|
@@ -17268,17 +17434,56 @@ var BaseResource = class {
|
|
|
17268
17434
|
const bigB = typeof b === "number" ? BigInt(String(b)) : BigInt(b);
|
|
17269
17435
|
return bigA < bigB ? -1 : bigA > bigB ? 1 : 0;
|
|
17270
17436
|
}
|
|
17437
|
+
/** Issue #127: the `latest` walk's count probes through the per-client
|
|
17438
|
+
* short-TTL count cache (`LatestCountCache` — fixed 30 s TTL, the
|
|
17439
|
+
* memory guard; cleared on every real write through this client, the
|
|
17440
|
+
* write-then-read re-fetch rule). Only a SUCCESSFUL plain-number vendor
|
|
17441
|
+
* count answer is stored: the blind-surface / pinned-empty sentinels are
|
|
17442
|
+
* RECORDS and pass through un-stored (the plan-pinned stamps stay fresh
|
|
17443
|
+
* and re-derive on every call), and an error rejects before any store.
|
|
17444
|
+
* The key is (resource plan key, canonical filter serialisation — the
|
|
17445
|
+
* normalised conditions' JSON): the initial `count(F)` probe and every
|
|
17446
|
+
* `id gt T` threshold probe are separate entries, and a re-asked
|
|
17447
|
+
* "top N" within the TTL makes ZERO extra count calls (its probes all
|
|
17448
|
+
* hit — the bracket/search sequence is deterministic given the counts).
|
|
17449
|
+
* Issue #157: two corrections to the snapshot trade. (1) A total of 0 is
|
|
17450
|
+
* NEVER stored — a 0 is rare and cheap to re-probe, and a cached 0 fed
|
|
17451
|
+
* the false-empty branch (a repeat after an out-of-band insert returned
|
|
17452
|
+
* no rows without walking). (2) `fresh: true` DELETES the entry before
|
|
17453
|
+
* the wire read and re-stores the fresh answer — the ONE re-probe the
|
|
17454
|
+
* stale-LOW recovery in `latestWalk` is allowed. Counts are snapshots,
|
|
17455
|
+
* not contracts: the truncation guard covers the growth-of-total
|
|
17456
|
+
* direction; a stale-LOW total within the TTL is resolved by that one
|
|
17457
|
+
* fresh re-probe (never by the guard alone). The plain `count` op and
|
|
17458
|
+
* `operations.aggregate` call `countGlobal` directly and promise fresh
|
|
17459
|
+
* totals — this wrapper is used ONLY on the latest-walk probe path.
|
|
17460
|
+
*/
|
|
17461
|
+
async latestProbeCount(filter, opts = {}) {
|
|
17462
|
+
const cache = this.http.latestCountCache;
|
|
17463
|
+
const key = LatestCountCache.key(this.config.key, normalizedConditions(filter));
|
|
17464
|
+
if (opts.fresh === true) {
|
|
17465
|
+
cache.delete(key);
|
|
17466
|
+
} else {
|
|
17467
|
+
const hit = cache.get(key);
|
|
17468
|
+
if (hit !== void 0) return { value: hit, cached: true };
|
|
17469
|
+
}
|
|
17470
|
+
const epoch = cache.epoch;
|
|
17471
|
+
const res = await this.countGlobal({ filter });
|
|
17472
|
+
const value = res;
|
|
17473
|
+
if (typeof value === "number" && value !== 0) cache.set(key, value, epoch);
|
|
17474
|
+
return { value, cached: false };
|
|
17475
|
+
}
|
|
17271
17476
|
/** Issue #59: one `latest` threshold count probe — a ROWLESS
|
|
17272
17477
|
* `count(F ∧ id gt T)` call (the vendor caps — ≤500 OR-conditions,
|
|
17273
17478
|
* 8000 chars serialised — leave a single `gt` condition room on any
|
|
17274
|
-
* filter). The `BlindCountResult` sentinel (a record, not a number —
|
|
17479
|
+
* filter). Issue #127: it rides the per-client short-TTL count cache — the same (resource, conditions) probe within the 30 s window is reused (zero vendor calls); a sentinel answer is never cached. The `BlindCountResult` sentinel (a record, not a number —
|
|
17275
17480
|
* the declared `number` return is the documented under-type) unwraps to
|
|
17276
17481
|
* its `count` value (always 0): at or above the max matching id the
|
|
17277
17482
|
* count is 0, which is exactly the bracket-termination signal. */
|
|
17278
17483
|
async countAboveId(baseConds, t) {
|
|
17279
17484
|
const value = t <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(t) : t.toString();
|
|
17280
|
-
const
|
|
17281
|
-
return typeof
|
|
17485
|
+
const probed = await this.latestProbeCount([...baseConds, { op: "gt", field: "id", value }]);
|
|
17486
|
+
return typeof probed.value === "object" && probed.value !== null && "count" in probed.value ? probed.value.count : probed.value;
|
|
17282
17487
|
}
|
|
17283
17488
|
/**
|
|
17284
17489
|
* Issue #59: the `latest: N` mechanism — the N highest-`id` rows,
|
|
@@ -17296,8 +17501,11 @@ var BaseResource = class {
|
|
|
17296
17501
|
* 1 + ceil(C/500) <= 6, independent of table size. A truncated walk
|
|
17297
17502
|
* (the vendor's rows disagree with its own count) is
|
|
17298
17503
|
* LATEST_WINDOW_INCOMPLETE — the top-N of a truncated set is
|
|
17299
|
-
* unprovable
|
|
17300
|
-
*
|
|
17504
|
+
* unprovable — EXCEPT issue #157: when `C` came from the CACHE, the
|
|
17505
|
+
* total is re-probed ONCE fresh (the stale-LOW recovery: a larger
|
|
17506
|
+
* fresh total continues the walk against it; the refusal stands only
|
|
17507
|
+
* when the fresh probe confirms the cached value). Sort
|
|
17508
|
+
* id-descending; return the top N (a set of C < N returns itself).
|
|
17301
17509
|
* - `C > 2500` → THRESHOLD mode: find the smallest id threshold `T`
|
|
17302
17510
|
* with `count(F ∧ id gt T) <= N` by exponential bracketing
|
|
17303
17511
|
* (T = 1, 2, 4, …) + binary search — each probe ONE rowless count
|
|
@@ -17324,13 +17532,25 @@ var BaseResource = class {
|
|
|
17324
17532
|
* answer.
|
|
17325
17533
|
* `includeFields` always carries `id` (the P1 id-append,
|
|
17326
17534
|
* `buildQueryRequest`), which `latest` sorts on. Pure read; no mutation
|
|
17327
|
-
* paths involved.
|
|
17535
|
+
* paths involved. Issue #127: every count probe of this walk (the
|
|
17536
|
+
* initial `count(F)` + the `id gt T` threshold probes) rides the
|
|
17537
|
+
* per-client short-TTL count cache (30 s; a real write through this
|
|
17538
|
+
* client clears it) — a re-asked "top N" within the window reuses the
|
|
17539
|
+
* probes (zero extra vendor count calls); sentinel answers are never
|
|
17540
|
+
* cached (the plan-pinned stamps stay fresh), and a 0 total is never
|
|
17541
|
+
* cached either (issue #157: a 0 is cheap to re-probe and a cached 0
|
|
17542
|
+
* would shadow an out-of-band insert). The stale-LOW direction is closed
|
|
17543
|
+
* by the ONE fresh re-probe above; the stale-HIGH direction is safe by
|
|
17544
|
+
* construction (threshold mode re-probes fresh `id gt T` counts and
|
|
17545
|
+
* full-set mode ends when the cursor exhausts). The plain `count` op and
|
|
17546
|
+
* `operations.aggregate` bypass the cache by construction and promise
|
|
17547
|
+
* fresh totals.
|
|
17328
17548
|
*/
|
|
17329
17549
|
async latestWalk(params, limit, latest) {
|
|
17330
17550
|
const baseConds = normalizedConditions(params.filter) ?? [];
|
|
17331
|
-
const probe = await this.
|
|
17332
|
-
if (typeof probe === "object" && probe !== null) {
|
|
17333
|
-
const rec = probe;
|
|
17551
|
+
const probe = await this.latestProbeCount(params.filter);
|
|
17552
|
+
if (typeof probe.value === "object" && probe.value !== null) {
|
|
17553
|
+
const rec = probe.value;
|
|
17334
17554
|
if (rec.blindSurface !== void 0) {
|
|
17335
17555
|
return this.latestBlindWalk(params, limit, latest);
|
|
17336
17556
|
}
|
|
@@ -17338,47 +17558,60 @@ var BaseResource = class {
|
|
|
17338
17558
|
this.stampEmptyQueryResult(result, params);
|
|
17339
17559
|
return result;
|
|
17340
17560
|
}
|
|
17341
|
-
|
|
17342
|
-
|
|
17343
|
-
|
|
17344
|
-
|
|
17345
|
-
|
|
17346
|
-
|
|
17347
|
-
|
|
17348
|
-
|
|
17349
|
-
if (this.walkTruncated(total, { itemsLength: items2.length, guard: guard2, sliced: sliced2, cursor: cursor2 })) {
|
|
17350
|
-
throw this.latestWindowIncompleteError(total);
|
|
17561
|
+
let total = probe.value;
|
|
17562
|
+
let cacheSourced = probe.cached;
|
|
17563
|
+
let reProbed = false;
|
|
17564
|
+
for (; ; ) {
|
|
17565
|
+
if (total === 0) {
|
|
17566
|
+
const result = { items: [], fetched: 0, hasMore: false, pagesWalked: 0 };
|
|
17567
|
+
this.stampEmptyQueryResult(result, params);
|
|
17568
|
+
return result;
|
|
17351
17569
|
}
|
|
17352
|
-
|
|
17353
|
-
|
|
17354
|
-
|
|
17355
|
-
|
|
17356
|
-
|
|
17357
|
-
|
|
17358
|
-
|
|
17359
|
-
|
|
17360
|
-
|
|
17361
|
-
|
|
17362
|
-
|
|
17363
|
-
|
|
17364
|
-
|
|
17365
|
-
|
|
17570
|
+
if (total <= BOUNDED_QUERY_MAX_LIMIT) {
|
|
17571
|
+
const { items: items2, pages: pages2, guard: guard2, sliced: sliced2, cursor: cursor2 } = await this.walkBounded(params, total);
|
|
17572
|
+
if (this.walkTruncated(total, { itemsLength: items2.length, guard: guard2, sliced: sliced2, cursor: cursor2 })) {
|
|
17573
|
+
if (cacheSourced && !reProbed) {
|
|
17574
|
+
reProbed = true;
|
|
17575
|
+
const fresh = await this.latestProbeCount(params.filter, { fresh: true });
|
|
17576
|
+
if (typeof fresh.value === "number" && fresh.value > total) {
|
|
17577
|
+
total = fresh.value;
|
|
17578
|
+
cacheSourced = false;
|
|
17579
|
+
continue;
|
|
17580
|
+
}
|
|
17581
|
+
}
|
|
17582
|
+
throw this.latestWindowIncompleteError(total);
|
|
17583
|
+
}
|
|
17584
|
+
return this.finishLatest(items2, latest, pages2);
|
|
17366
17585
|
}
|
|
17367
|
-
|
|
17368
|
-
|
|
17369
|
-
|
|
17370
|
-
|
|
17371
|
-
|
|
17372
|
-
|
|
17373
|
-
|
|
17374
|
-
|
|
17375
|
-
|
|
17376
|
-
|
|
17377
|
-
|
|
17378
|
-
|
|
17379
|
-
|
|
17586
|
+
let probes = reProbed ? 2 : 1;
|
|
17587
|
+
const spend = () => {
|
|
17588
|
+
probes += 1;
|
|
17589
|
+
if (probes > LATEST_MAX_COUNT_PROBES) throw this.latestThresholdUnresolvedError();
|
|
17590
|
+
};
|
|
17591
|
+
let lo = 0n;
|
|
17592
|
+
let hi = 0n;
|
|
17593
|
+
for (let t = 1n; ; t = t << 1n) {
|
|
17594
|
+
spend();
|
|
17595
|
+
if (await this.countAboveId(baseConds, t) <= latest) {
|
|
17596
|
+
hi = t;
|
|
17597
|
+
break;
|
|
17598
|
+
}
|
|
17599
|
+
lo = t;
|
|
17600
|
+
}
|
|
17601
|
+
while (hi - lo > 1n) {
|
|
17602
|
+
const mid = lo + hi >> 1n;
|
|
17603
|
+
spend();
|
|
17604
|
+
if (await this.countAboveId(baseConds, mid) <= latest) hi = mid;
|
|
17605
|
+
else lo = mid;
|
|
17606
|
+
}
|
|
17607
|
+
const windowValue = hi <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(hi) : hi.toString();
|
|
17608
|
+
const window = [...baseConds, { op: "gte", field: "id", value: windowValue }];
|
|
17609
|
+
const { items, pages, guard, sliced, cursor } = await this.walkBounded({ ...params, filter: window }, latest + 1);
|
|
17610
|
+
if (this.walkTruncated(latest + 1, { itemsLength: items.length, guard, sliced, cursor })) {
|
|
17611
|
+
throw this.latestWindowIncompleteError(latest + 1);
|
|
17612
|
+
}
|
|
17613
|
+
return this.finishLatest(items, latest, pages);
|
|
17380
17614
|
}
|
|
17381
|
-
return this.finishLatest(items, latest, pages);
|
|
17382
17615
|
}
|
|
17383
17616
|
/** The shared `latest` completion: the (provably complete or
|
|
17384
17617
|
* stamp-scoped) set sorted id-descending, top N taken. `hasMore: false`
|
|
@@ -18702,6 +18935,7 @@ export {
|
|
|
18702
18935
|
DEFAULT_MAX_RETRIES,
|
|
18703
18936
|
DEFAULT_MAINTENANCE_MAX_WAIT_MS,
|
|
18704
18937
|
DEFAULT_MAINTENANCE_POLL_MS,
|
|
18938
|
+
MAINTENANCE_WINDOW_MAX_MS,
|
|
18705
18939
|
DEFAULT_MAX_CONCURRENT,
|
|
18706
18940
|
DEFAULT_MAX_RESPONSE_BYTES,
|
|
18707
18941
|
DEFAULT_MAX_SCAN_RECORDS,
|
|
@@ -127,13 +127,13 @@ import {
|
|
|
127
127
|
VersionInformationResource,
|
|
128
128
|
VersionResource,
|
|
129
129
|
ZoneInformationResource
|
|
130
|
-
} from "./chunk-
|
|
130
|
+
} from "./chunk-XSYJB4IW.js";
|
|
131
131
|
import {
|
|
132
132
|
AutotaskConfigError,
|
|
133
133
|
HttpClient,
|
|
134
134
|
resolveConfig,
|
|
135
135
|
runWithSignal
|
|
136
|
-
} from "./chunk-
|
|
136
|
+
} from "./chunk-GA6UCX3Z.js";
|
|
137
137
|
|
|
138
138
|
// src/client.ts
|
|
139
139
|
var AutotaskClient = class {
|