@salesforce/agentic-common 0.1.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.
@@ -0,0 +1,52 @@
1
+ /*
2
+ * Copyright 2026, Salesforce, Inc. All rights reserved.
3
+ * See LICENSE.txt for license terms.
4
+ */
5
+ /**
6
+ * Minimal, typed, object-bound event bus. Carries a single event shape `T`.
7
+ *
8
+ * Listener errors are isolated: a throwing callback never interrupts `emit()` or other listeners.
9
+ * `on()` returns an `Unsubscribe` closure so callers don't need to retain the original callback reference.
10
+ */
11
+ export class EventBus {
12
+ listeners = new Set();
13
+ /** Number of currently registered listeners. Useful for leak-check assertions in tests. */
14
+ get listenerCount() {
15
+ return this.listeners.size;
16
+ }
17
+ /**
18
+ * Subscribe to events. Returns a function that unregisters this specific callback.
19
+ * Safe to call the returned function multiple times — subsequent calls are no-ops.
20
+ */
21
+ on(callback) {
22
+ this.listeners.add(callback);
23
+ return () => {
24
+ this.listeners.delete(callback);
25
+ };
26
+ }
27
+ /** Emit an event to every registered listener. Listener errors are caught and discarded. */
28
+ emit(event) {
29
+ for (const listener of this.listeners) {
30
+ try {
31
+ listener(event);
32
+ }
33
+ catch {
34
+ // Intentional: one bad listener must not break siblings or the emitter.
35
+ }
36
+ }
37
+ }
38
+ /**
39
+ * Subscribe to this bus and re-emit every event onto `target`. If `enrich` is supplied, each event is
40
+ * passed through it before re-emission. Returns an `Unsubscribe` for the internal subscription.
41
+ */
42
+ forwardTo(target, enrich) {
43
+ return this.on((event) => {
44
+ target.emit(enrich ? enrich(event) : event);
45
+ });
46
+ }
47
+ /** Remove all listeners. Idempotent. */
48
+ dispose() {
49
+ this.listeners.clear();
50
+ }
51
+ }
52
+ //# sourceMappingURL=event-bus.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"event-bus.js","sourceRoot":"","sources":["../src/event-bus.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH;;;;;GAKG;AACH,MAAM,OAAO,QAAQ;IACA,SAAS,GAA0B,IAAI,GAAG,EAAE,CAAC;IAE9D,2FAA2F;IAC3F,IAAI,aAAa;QACb,OAAO,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;IAC/B,CAAC;IAED;;;OAGG;IACH,EAAE,CAAC,QAA0B;QACzB,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAC7B,OAAO,GAAG,EAAE;YACR,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QACpC,CAAC,CAAC;IACN,CAAC;IAED,4FAA4F;IAC5F,IAAI,CAAC,KAAQ;QACT,KAAK,MAAM,QAAQ,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;YACpC,IAAI,CAAC;gBACD,QAAQ,CAAC,KAAK,CAAC,CAAC;YACpB,CAAC;YAAC,MAAM,CAAC;gBACL,wEAAwE;YAC5E,CAAC;QACL,CAAC;IACL,CAAC;IAED;;;OAGG;IACH,SAAS,CAAC,MAAmB,EAAE,MAAwB;QACnD,OAAO,IAAI,CAAC,EAAE,CAAC,CAAC,KAAK,EAAE,EAAE;YACrB,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;QAChD,CAAC,CAAC,CAAC;IACP,CAAC;IAED,wCAAwC;IACxC,OAAO;QACH,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,CAAC;IAC3B,CAAC;CACJ"}
@@ -0,0 +1,6 @@
1
+ export interface UniqueIDGenerator {
2
+ getUniqueId(): string;
3
+ }
4
+ export declare class UUIDGenerator implements UniqueIDGenerator {
5
+ getUniqueId(): string;
6
+ }
@@ -0,0 +1,11 @@
1
+ /*
2
+ * Copyright 2026, Salesforce, Inc. All rights reserved.
3
+ * See LICENSE.txt for license terms.
4
+ */
5
+ import { randomUUID } from 'node:crypto';
6
+ export class UUIDGenerator {
7
+ getUniqueId() {
8
+ return randomUUID();
9
+ }
10
+ }
11
+ //# sourceMappingURL=id-generator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"id-generator.js","sourceRoot":"","sources":["../src/id-generator.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAMzC,MAAM,OAAO,aAAa;IACtB,WAAW;QACP,OAAO,UAAU,EAAE,CAAC;IACxB,CAAC;CACJ"}
@@ -0,0 +1,9 @@
1
+ export { Clock, RealClock } from './clock.js';
2
+ export { type OrgConnection, RealOrgConnection } from './connection.js';
3
+ export { type OrgConnectionFactory, RealOrgConnectionFactory } from './connection-factory.js';
4
+ export { getErrorMessage, getErrorMessageWithStack, isAbortError, wrapError } from './error-utils.js';
5
+ export { EventBus, type EventListener, type Unsubscribe } from './event-bus.js';
6
+ export { type UniqueIDGenerator, UUIDGenerator } from './id-generator.js';
7
+ export { LogBus, type LogLevel, type LogRecord } from './log.js';
8
+ export { BackoffRetryer, DEFAULT_RETRY_OPTIONS, NoOpRetryer, type Retryer, type RetryAttemptInfo, type RetryCallbacks, type RetryExhaustedInfo, type RetryOptions, } from './retryer.js';
9
+ export { SfApiEnv } from './sf-api-env.js';
package/dist/index.js ADDED
@@ -0,0 +1,14 @@
1
+ /*
2
+ * Copyright 2026, Salesforce, Inc. All rights reserved.
3
+ * See LICENSE.txt for license terms.
4
+ */
5
+ export { Clock, RealClock } from './clock.js';
6
+ export { RealOrgConnection } from './connection.js';
7
+ export { RealOrgConnectionFactory } from './connection-factory.js';
8
+ export { getErrorMessage, getErrorMessageWithStack, isAbortError, wrapError } from './error-utils.js';
9
+ export { EventBus } from './event-bus.js';
10
+ export { UUIDGenerator } from './id-generator.js';
11
+ export { LogBus } from './log.js';
12
+ export { BackoffRetryer, DEFAULT_RETRY_OPTIONS, NoOpRetryer, } from './retryer.js';
13
+ export { SfApiEnv } from './sf-api-env.js';
14
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAC9C,OAAO,EAAsB,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACxE,OAAO,EAA6B,wBAAwB,EAAE,MAAM,yBAAyB,CAAC;AAC9F,OAAO,EAAE,eAAe,EAAE,wBAAwB,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AACtG,OAAO,EAAE,QAAQ,EAAwC,MAAM,gBAAgB,CAAC;AAChF,OAAO,EAA0B,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAC1E,OAAO,EAAE,MAAM,EAAiC,MAAM,UAAU,CAAC;AACjE,OAAO,EACH,cAAc,EACd,qBAAqB,EACrB,WAAW,GAMd,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC"}
package/dist/log.d.ts ADDED
@@ -0,0 +1,26 @@
1
+ import { EventBus } from './event-bus.js';
2
+ export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
3
+ /**
4
+ * Structured log record shared across the agentic DX packages.
5
+ *
6
+ * Consumers of the SDKs receive `LogRecord`s through an `onLog()` subscription; the SDKs themselves do not
7
+ * depend on any logging library and do not write to `stdout` / `stderr` directly.
8
+ */
9
+ export type LogRecord = {
10
+ level: LogLevel;
11
+ message: string;
12
+ timestamp: Date;
13
+ context?: Record<string, unknown>;
14
+ error?: Error;
15
+ };
16
+ /**
17
+ * Typed event bus carrying `LogRecord`s. Adds level-named convenience methods (`debug` / `info` / `warn` /
18
+ * `error`) that build a record (timestamp + level) and emit it, so emit sites read as
19
+ * `bus.warn('message', { ctx })` instead of `logWarn(bus, 'message', { ctx })`.
20
+ */
21
+ export declare class LogBus extends EventBus<LogRecord> {
22
+ debug(message: string, context?: Record<string, unknown>): void;
23
+ info(message: string, context?: Record<string, unknown>): void;
24
+ warn(message: string, context?: Record<string, unknown>, error?: Error): void;
25
+ error(message: string, error?: Error, context?: Record<string, unknown>): void;
26
+ }
package/dist/log.js ADDED
@@ -0,0 +1,25 @@
1
+ /*
2
+ * Copyright 2026, Salesforce, Inc. All rights reserved.
3
+ * See LICENSE.txt for license terms.
4
+ */
5
+ import { EventBus } from './event-bus.js';
6
+ /**
7
+ * Typed event bus carrying `LogRecord`s. Adds level-named convenience methods (`debug` / `info` / `warn` /
8
+ * `error`) that build a record (timestamp + level) and emit it, so emit sites read as
9
+ * `bus.warn('message', { ctx })` instead of `logWarn(bus, 'message', { ctx })`.
10
+ */
11
+ export class LogBus extends EventBus {
12
+ debug(message, context) {
13
+ this.emit({ level: 'debug', message, timestamp: new Date(), context });
14
+ }
15
+ info(message, context) {
16
+ this.emit({ level: 'info', message, timestamp: new Date(), context });
17
+ }
18
+ warn(message, context, error) {
19
+ this.emit({ level: 'warn', message, timestamp: new Date(), context, error });
20
+ }
21
+ error(message, error, context) {
22
+ this.emit({ level: 'error', message, timestamp: new Date(), context, error });
23
+ }
24
+ }
25
+ //# sourceMappingURL=log.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"log.js","sourceRoot":"","sources":["../src/log.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAkB1C;;;;GAIG;AACH,MAAM,OAAO,MAAO,SAAQ,QAAmB;IAC3C,KAAK,CAAC,OAAe,EAAE,OAAiC;QACpD,IAAI,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,IAAI,IAAI,EAAE,EAAE,OAAO,EAAE,CAAC,CAAC;IAC3E,CAAC;IAED,IAAI,CAAC,OAAe,EAAE,OAAiC;QACnD,IAAI,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,IAAI,IAAI,EAAE,EAAE,OAAO,EAAE,CAAC,CAAC;IAC1E,CAAC;IAED,IAAI,CAAC,OAAe,EAAE,OAAiC,EAAE,KAAa;QAClE,IAAI,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,IAAI,IAAI,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;IACjF,CAAC;IAED,KAAK,CAAC,OAAe,EAAE,KAAa,EAAE,OAAiC;QACnE,IAAI,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,IAAI,IAAI,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;IAClF,CAAC;CACJ"}
@@ -0,0 +1,137 @@
1
+ import { Clock } from './clock.js';
2
+ /**
3
+ * Public retry configuration. The retryer applies exponential backoff with full jitter, optional
4
+ * server-driven delay hints, and a wall-clock deadline. All fields are optional; defaults are
5
+ * `DEFAULT_RETRY_OPTIONS`.
6
+ */
7
+ export type RetryOptions = {
8
+ /** Total attempts including the first. Default: 3. Must be >= 1. To disable retries, pass `false`. */
9
+ maxAttempts?: number;
10
+ /** Initial delay before the first retry, in ms. Default: 100. */
11
+ initialDelayMs?: number;
12
+ /** Maximum delay between *computed* exponential-backoff retries, in ms. Default: 2000. */
13
+ maxDelayMs?: number;
14
+ /**
15
+ * Maximum delay accepted from a server-driven hint (e.g. `Retry-After`), in ms. Hints exceeding
16
+ * this are honored as "do not retry, surface the result" so the caller sees the structured
17
+ * response instead of the retryer waiting too long. Default: 60000.
18
+ */
19
+ maxRetryAfterMs?: number;
20
+ /** Exponential backoff multiplier. Default: 2. */
21
+ backoffFactor?: number;
22
+ /**
23
+ * Hard ceiling on cumulative wall-clock time across all attempts and inter-attempt sleeps,
24
+ * in ms. When the next sleep would push past this deadline the loop bails and surfaces the
25
+ * most recent error / result instead of waiting. Default: `Infinity`.
26
+ */
27
+ maxTotalElapsedMs?: number;
28
+ };
29
+ export type ResolvedRetryOptions = Required<RetryOptions>;
30
+ export declare const DEFAULT_RETRY_OPTIONS: ResolvedRetryOptions;
31
+ export declare function resolveRetryOptions(opts?: RetryOptions | false): ResolvedRetryOptions;
32
+ /**
33
+ * Deterministic exponential backoff ceiling for an attempt number (1-indexed).
34
+ * `attempt 1 → initialDelayMs`, `attempt 2 → initialDelayMs * factor`, capped at `maxDelayMs`.
35
+ */
36
+ export declare function computeBackoffCeilingMs(attempt: number, opts: ResolvedRetryOptions): number;
37
+ /**
38
+ * Apply full jitter: pick a uniform random value in `[0, ceilingMs]`. Spreads concurrent retriers
39
+ * across the backoff window instead of synchronizing them at the same moment.
40
+ */
41
+ export declare function applyFullJitter(ceilingMs: number, random: () => number): number;
42
+ /**
43
+ * Sleep for `ms` milliseconds, honoring an optional `AbortSignal`. Rejects with an
44
+ * `AbortError`-named error if the signal aborts before the delay elapses.
45
+ */
46
+ export declare const realSleep: (ms: number, signal?: AbortSignal) => Promise<void>;
47
+ export type RetryAttemptInfo<T> = {
48
+ /** 1-indexed attempt number that just failed. */
49
+ attempt: number;
50
+ /** Jittered or server-driven delay about to be waited. */
51
+ delayMs: number;
52
+ /** Set if the attempt threw a retryable error. */
53
+ error?: unknown;
54
+ /** Set if the attempt returned a retryable result. */
55
+ result?: T;
56
+ };
57
+ export type RetryExhaustedInfo<T> = {
58
+ /** Total attempts made. */
59
+ attempts: number;
60
+ /** The final retryable error, if the last attempt threw. */
61
+ error?: unknown;
62
+ /** The final retryable result, if the last attempt returned one. */
63
+ result?: T;
64
+ /** Why the loop terminated: ran out of attempts, or hit the wall-clock deadline. */
65
+ reason: 'attempts' | 'deadline';
66
+ };
67
+ /**
68
+ * Callbacks the caller provides to customize retry behavior for a specific request/response shape.
69
+ * All are optional — sensible defaults apply (no errors retried, no results retried, no server hints).
70
+ */
71
+ export type RetryCallbacks<T> = {
72
+ /** AbortSignal to cancel the loop (propagates immediately on abort). */
73
+ signal?: AbortSignal;
74
+ /**
75
+ * Classify a thrown error as retryable. Return `true` to retry, `false` to propagate.
76
+ * Default: always `false` (no errors retried).
77
+ */
78
+ isRetryableError?: (err: unknown) => boolean;
79
+ /**
80
+ * Classify a successful result as retryable (e.g. HTTP 429 / 5xx). Return `true` to retry.
81
+ * Default: always `false` (all results accepted).
82
+ */
83
+ isRetryableResult?: (result: T) => boolean;
84
+ /**
85
+ * Extract a server-driven retry delay from a retryable result (e.g. `Retry-After` header).
86
+ * Return `undefined` to fall back to computed backoff. Return a value > `maxRetryAfterMs`
87
+ * to signal "don't retry, surface this result to the caller".
88
+ */
89
+ getRetryAfterMs?: (result: T) => number | undefined;
90
+ /** Fired before each retry sleep. Use for telemetry, logging, metrics. */
91
+ onRetry?: (info: RetryAttemptInfo<T>) => void;
92
+ /**
93
+ * Fired when retries are exhausted (final attempt returned a retryable result or threw a
94
+ * retryable error). Use for "retries exhausted" warnings. Not fired when a non-retryable
95
+ * result/error terminates the loop.
96
+ */
97
+ onExhausted?: (info: RetryExhaustedInfo<T>) => void;
98
+ /**
99
+ * Release resources from a retryable result before retrying (e.g. drain HTTP response body
100
+ * to release the socket). Errors are swallowed — the retry proceeds regardless.
101
+ */
102
+ drainResult?: (result: T) => Promise<void>;
103
+ };
104
+ /**
105
+ * A generic retry executor. Consumers depend on this interface — they never see backoff internals,
106
+ * sleep functions, or RNG. Those are construction details of the implementation.
107
+ */
108
+ export interface Retryer {
109
+ execute<T>(attemptFn: () => Promise<T>, callbacks?: RetryCallbacks<T>): Promise<T>;
110
+ }
111
+ export type BackoffRetryerDeps = {
112
+ clock: Clock;
113
+ sleep: (ms: number, signal?: AbortSignal) => Promise<void>;
114
+ random: () => number;
115
+ };
116
+ /**
117
+ * Retry executor using exponential backoff with full jitter, optional server-driven delay hints,
118
+ * and a wall-clock deadline. Honors `AbortSignal` at every decision point.
119
+ *
120
+ * Domain-specific retry decisions (which errors / status codes are retryable, how to extract a
121
+ * `Retry-After` value, how to drain a response body, what to log) are passed via `RetryCallbacks`
122
+ * at the call site — the retryer itself is protocol-agnostic.
123
+ */
124
+ export declare class BackoffRetryer implements Retryer {
125
+ private readonly options;
126
+ private readonly deps;
127
+ constructor(options?: RetryOptions | false, deps?: Partial<BackoffRetryerDeps>);
128
+ execute<T>(attemptFn: () => Promise<T>, callbacks?: RetryCallbacks<T>): Promise<T>;
129
+ private wouldExceedDeadline;
130
+ }
131
+ /**
132
+ * A pass-through retryer that executes the attempt exactly once with no retry. Use in tests where
133
+ * retry behavior is irrelevant to the system under test.
134
+ */
135
+ export declare class NoOpRetryer implements Retryer {
136
+ execute<T>(attemptFn: () => Promise<T>): Promise<T>;
137
+ }
@@ -0,0 +1,175 @@
1
+ /*
2
+ * Copyright 2026, Salesforce, Inc. All rights reserved.
3
+ * See LICENSE.txt for license terms.
4
+ */
5
+ import { Clock, RealClock } from './clock.js';
6
+ import { isAbortError } from './error-utils.js';
7
+ export const DEFAULT_RETRY_OPTIONS = {
8
+ maxAttempts: 3,
9
+ initialDelayMs: 100,
10
+ maxDelayMs: 2000,
11
+ maxRetryAfterMs: 60_000,
12
+ backoffFactor: 2,
13
+ maxTotalElapsedMs: Number.POSITIVE_INFINITY,
14
+ };
15
+ export function resolveRetryOptions(opts) {
16
+ if (opts === false) {
17
+ return { ...DEFAULT_RETRY_OPTIONS, maxAttempts: 1 };
18
+ }
19
+ const maxAttempts = opts?.maxAttempts ?? DEFAULT_RETRY_OPTIONS.maxAttempts;
20
+ if (!Number.isInteger(maxAttempts) || maxAttempts < 1) {
21
+ throw new Error(`Invalid retry option: maxAttempts must be an integer >= 1 (got ${maxAttempts}). Pass retry: false to disable retries.`);
22
+ }
23
+ return {
24
+ maxAttempts,
25
+ initialDelayMs: opts?.initialDelayMs ?? DEFAULT_RETRY_OPTIONS.initialDelayMs,
26
+ maxDelayMs: opts?.maxDelayMs ?? DEFAULT_RETRY_OPTIONS.maxDelayMs,
27
+ maxRetryAfterMs: opts?.maxRetryAfterMs ?? DEFAULT_RETRY_OPTIONS.maxRetryAfterMs,
28
+ backoffFactor: opts?.backoffFactor ?? DEFAULT_RETRY_OPTIONS.backoffFactor,
29
+ maxTotalElapsedMs: opts?.maxTotalElapsedMs ?? DEFAULT_RETRY_OPTIONS.maxTotalElapsedMs,
30
+ };
31
+ }
32
+ /**
33
+ * Deterministic exponential backoff ceiling for an attempt number (1-indexed).
34
+ * `attempt 1 → initialDelayMs`, `attempt 2 → initialDelayMs * factor`, capped at `maxDelayMs`.
35
+ */
36
+ export function computeBackoffCeilingMs(attempt, opts) {
37
+ if (attempt < 1) {
38
+ return 0;
39
+ }
40
+ const exponential = opts.initialDelayMs * Math.pow(opts.backoffFactor, attempt - 1);
41
+ return Math.min(exponential, opts.maxDelayMs);
42
+ }
43
+ /**
44
+ * Apply full jitter: pick a uniform random value in `[0, ceilingMs]`. Spreads concurrent retriers
45
+ * across the backoff window instead of synchronizing them at the same moment.
46
+ */
47
+ export function applyFullJitter(ceilingMs, random) {
48
+ return Math.floor(random() * (ceilingMs + 1));
49
+ }
50
+ /**
51
+ * Sleep for `ms` milliseconds, honoring an optional `AbortSignal`. Rejects with an
52
+ * `AbortError`-named error if the signal aborts before the delay elapses.
53
+ */
54
+ export const realSleep = (ms, signal) => new Promise((resolve, reject) => {
55
+ if (signal?.aborted) {
56
+ reject(makeAbortError());
57
+ return;
58
+ }
59
+ const onAbort = () => {
60
+ clearTimeout(timer);
61
+ reject(makeAbortError());
62
+ };
63
+ const timer = setTimeout(() => {
64
+ signal?.removeEventListener('abort', onAbort);
65
+ resolve();
66
+ }, ms);
67
+ signal?.addEventListener('abort', onAbort, { once: true });
68
+ });
69
+ function makeAbortError() {
70
+ const err = new Error('Aborted');
71
+ err.name = 'AbortError';
72
+ return err;
73
+ }
74
+ /**
75
+ * Retry executor using exponential backoff with full jitter, optional server-driven delay hints,
76
+ * and a wall-clock deadline. Honors `AbortSignal` at every decision point.
77
+ *
78
+ * Domain-specific retry decisions (which errors / status codes are retryable, how to extract a
79
+ * `Retry-After` value, how to drain a response body, what to log) are passed via `RetryCallbacks`
80
+ * at the call site — the retryer itself is protocol-agnostic.
81
+ */
82
+ export class BackoffRetryer {
83
+ options;
84
+ deps;
85
+ constructor(options, deps) {
86
+ this.options = resolveRetryOptions(options);
87
+ this.deps = {
88
+ clock: deps?.clock ?? new RealClock(),
89
+ sleep: deps?.sleep ?? realSleep,
90
+ random: deps?.random ?? Math.random,
91
+ };
92
+ }
93
+ async execute(attemptFn, callbacks = {}) {
94
+ const { signal, isRetryableError = () => false, isRetryableResult = () => false, getRetryAfterMs, onRetry, onExhausted, drainResult, } = callbacks;
95
+ const { maxAttempts, maxRetryAfterMs, maxTotalElapsedMs } = this.options;
96
+ const startMs = this.deps.clock.now().getTime();
97
+ let lastError;
98
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
99
+ const isLast = attempt === maxAttempts;
100
+ let result;
101
+ try {
102
+ result = await attemptFn();
103
+ }
104
+ catch (err) {
105
+ if (signal?.aborted || isAbortError(err)) {
106
+ throw err;
107
+ }
108
+ if (!isRetryableError(err) || isLast) {
109
+ if (isLast && isRetryableError(err)) {
110
+ onExhausted?.({ attempts: attempt, error: err, reason: 'attempts' });
111
+ }
112
+ throw err;
113
+ }
114
+ const delayMs = applyFullJitter(computeBackoffCeilingMs(attempt, this.options), this.deps.random);
115
+ if (this.wouldExceedDeadline(startMs, delayMs, maxTotalElapsedMs)) {
116
+ onExhausted?.({ attempts: attempt, error: err, reason: 'deadline' });
117
+ throw err;
118
+ }
119
+ lastError = err;
120
+ onRetry?.({ attempt, delayMs, error: err });
121
+ await this.deps.sleep(delayMs, signal);
122
+ continue;
123
+ }
124
+ if (!isRetryableResult(result) || isLast) {
125
+ if (isLast && isRetryableResult(result)) {
126
+ onExhausted?.({ attempts: attempt, result, reason: 'attempts' });
127
+ }
128
+ return result;
129
+ }
130
+ const serverDelayMs = getRetryAfterMs?.(result);
131
+ if (serverDelayMs !== undefined && serverDelayMs > maxRetryAfterMs) {
132
+ // Server hinted longer than we're willing to wait — surface the result so the
133
+ // caller sees the structured error rather than the retryer waiting too long.
134
+ return result;
135
+ }
136
+ // Server-driven hint takes precedence verbatim — no jitter, no `maxDelayMs` clamp
137
+ // (which bounds *computed* backoff only).
138
+ const delayMs = serverDelayMs !== undefined
139
+ ? serverDelayMs
140
+ : applyFullJitter(computeBackoffCeilingMs(attempt, this.options), this.deps.random);
141
+ if (this.wouldExceedDeadline(startMs, delayMs, maxTotalElapsedMs)) {
142
+ onExhausted?.({ attempts: attempt, result, reason: 'deadline' });
143
+ return result;
144
+ }
145
+ onRetry?.({ attempt, delayMs, result });
146
+ if (drainResult) {
147
+ try {
148
+ await drainResult(result);
149
+ }
150
+ catch {
151
+ // Resource is gone either way; proceed to the retry sleep.
152
+ }
153
+ }
154
+ await this.deps.sleep(delayMs, signal);
155
+ }
156
+ // Loop only exits via return / throw above; this satisfies the type system.
157
+ throw lastError ?? new Error('Retry loop exited without a response');
158
+ }
159
+ wouldExceedDeadline(startMs, delayMs, maxTotalElapsedMs) {
160
+ if (!Number.isFinite(maxTotalElapsedMs)) {
161
+ return false;
162
+ }
163
+ return this.deps.clock.now().getTime() - startMs + delayMs > maxTotalElapsedMs;
164
+ }
165
+ }
166
+ /**
167
+ * A pass-through retryer that executes the attempt exactly once with no retry. Use in tests where
168
+ * retry behavior is irrelevant to the system under test.
169
+ */
170
+ export class NoOpRetryer {
171
+ async execute(attemptFn) {
172
+ return attemptFn();
173
+ }
174
+ }
175
+ //# sourceMappingURL=retryer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"retryer.js","sourceRoot":"","sources":["../src/retryer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ export declare enum SfApiEnv {
2
+ Dev = "dev",
3
+ Perf = "perf",
4
+ Prod = "prod",
5
+ Stage = "stage",
6
+ Test = "test"
7
+ }
8
+ /**
9
+ * Infers the Salesforce API environment from an instance URL, with `SF_API_ENV`
10
+ * as an explicit override.
11
+ *
12
+ * Resolution:
13
+ * 1. If `SF_API_ENV` is set to a valid value, use it.
14
+ * 2. Otherwise infer from the instance URL:
15
+ * - STM patterns → `stage`
16
+ * - OrgFarm dev patterns (`.devX.*.pc-rnd.*`) or workspaces (`.crm.dev`) → `dev`
17
+ * - OrgFarm perf patterns (`.perfXX.*.pc-rnd.*`) → `perf`
18
+ * - OrgFarm test patterns (`.testX.*.pc-rnd.*`) → `test`
19
+ * - Everything else (including unrecognized internal URLs) → `prod`
20
+ */
21
+ export declare function inferSfApiEnv(instanceUrl: string): SfApiEnv;
@@ -0,0 +1,56 @@
1
+ /*
2
+ * Copyright 2026, Salesforce, Inc. All rights reserved.
3
+ * See LICENSE.txt for license terms.
4
+ */
5
+ export var SfApiEnv;
6
+ (function (SfApiEnv) {
7
+ SfApiEnv["Dev"] = "dev";
8
+ SfApiEnv["Perf"] = "perf";
9
+ SfApiEnv["Prod"] = "prod";
10
+ SfApiEnv["Stage"] = "stage";
11
+ SfApiEnv["Test"] = "test";
12
+ })(SfApiEnv || (SfApiEnv = {}));
13
+ /**
14
+ * Infers the Salesforce API environment from an instance URL, with `SF_API_ENV`
15
+ * as an explicit override.
16
+ *
17
+ * Resolution:
18
+ * 1. If `SF_API_ENV` is set to a valid value, use it.
19
+ * 2. Otherwise infer from the instance URL:
20
+ * - STM patterns → `stage`
21
+ * - OrgFarm dev patterns (`.devX.*.pc-rnd.*`) or workspaces (`.crm.dev`) → `dev`
22
+ * - OrgFarm perf patterns (`.perfXX.*.pc-rnd.*`) → `perf`
23
+ * - OrgFarm test patterns (`.testX.*.pc-rnd.*`) → `test`
24
+ * - Everything else (including unrecognized internal URLs) → `prod`
25
+ */
26
+ export function inferSfApiEnv(instanceUrl) {
27
+ const envVar = process.env['SF_API_ENV']?.toLowerCase();
28
+ if (envVar && Object.values(SfApiEnv).includes(envVar)) {
29
+ return envVar;
30
+ }
31
+ const lower = instanceUrl.toLowerCase();
32
+ if (lower.includes('stm.salesforce.com') ||
33
+ lower.includes('stm.force.com') ||
34
+ lower.includes('.stm.salesforce.ms')) {
35
+ return SfApiEnv.Stage;
36
+ }
37
+ if (isPcRndUrl(lower)) {
38
+ if (/\.dev[a-z0-9]+\./.test(lower)) {
39
+ return SfApiEnv.Dev;
40
+ }
41
+ if (/\.perf\d\w*\./.test(lower)) {
42
+ return SfApiEnv.Perf;
43
+ }
44
+ if (/\.test\d+\./.test(lower)) {
45
+ return SfApiEnv.Test;
46
+ }
47
+ }
48
+ if (lower.includes('.crm.dev') || lower.includes('localhost.sfdcdev.') || lower.includes('.internal.')) {
49
+ return SfApiEnv.Dev;
50
+ }
51
+ return SfApiEnv.Prod;
52
+ }
53
+ function isPcRndUrl(lower) {
54
+ return lower.includes('.pc-rnd.salesforce.com') || lower.includes('.pc-rnd.force.com');
55
+ }
56
+ //# sourceMappingURL=sf-api-env.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sf-api-env.js","sourceRoot":"","sources":["../src/sf-api-env.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,CAAN,IAAY,QAMX;AAND,WAAY,QAAQ;IAChB,uBAAW,CAAA;IACX,yBAAa,CAAA;IACb,yBAAa,CAAA;IACb,2BAAe,CAAA;IACf,yBAAa,CAAA;AACjB,CAAC,EANW,QAAQ,KAAR,QAAQ,QAMnB;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,aAAa,CAAC,WAAmB;IAC7C,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,EAAE,WAAW,EAAE,CAAC;IACxD,IAAI,MAAM,IAAK,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAc,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;QACnE,OAAO,MAAkB,CAAC;IAC9B,CAAC;IAED,MAAM,KAAK,GAAG,WAAW,CAAC,WAAW,EAAE,CAAC;IACxC,IACI,KAAK,CAAC,QAAQ,CAAC,oBAAoB,CAAC;QACpC,KAAK,CAAC,QAAQ,CAAC,eAAe,CAAC;QAC/B,KAAK,CAAC,QAAQ,CAAC,oBAAoB,CAAC,EACtC,CAAC;QACC,OAAO,QAAQ,CAAC,KAAK,CAAC;IAC1B,CAAC;IAED,IAAI,UAAU,CAAC,KAAK,CAAC,EAAE,CAAC;QACpB,IAAI,kBAAkB,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;YACjC,OAAO,QAAQ,CAAC,GAAG,CAAC;QACxB,CAAC;QACD,IAAI,eAAe,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;YAC9B,OAAO,QAAQ,CAAC,IAAI,CAAC;QACzB,CAAC;QACD,IAAI,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;YAC5B,OAAO,QAAQ,CAAC,IAAI,CAAC;QACzB,CAAC;IACL,CAAC;IAED,IAAI,KAAK,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,oBAAoB,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,YAAY,CAAC,EAAE,CAAC;QACrG,OAAO,QAAQ,CAAC,GAAG,CAAC;IACxB,CAAC;IAED,OAAO,QAAQ,CAAC,IAAI,CAAC;AACzB,CAAC;AAED,SAAS,UAAU,CAAC,KAAa;IAC7B,OAAO,KAAK,CAAC,QAAQ,CAAC,wBAAwB,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,mBAAmB,CAAC,CAAC;AAC3F,CAAC"}