@project-ajax/sdk 0.0.75 → 0.0.76
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/capabilities/sync.d.ts +6 -2
- package/dist/capabilities/sync.d.ts.map +1 -1
- package/dist/capabilities/sync.js +16 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/pacer.d.ts +50 -0
- package/dist/pacer.d.ts.map +1 -0
- package/dist/pacer.js +48 -0
- package/dist/pacer.test.d.ts +2 -0
- package/dist/pacer.test.d.ts.map +1 -0
- package/dist/pacer_internal.d.ts +6 -0
- package/dist/pacer_internal.d.ts.map +1 -0
- package/dist/pacer_internal.js +11 -0
- package/package.json +5 -1
- package/src/capabilities/sync.ts +28 -3
- package/src/index.ts +2 -0
- package/src/pacer.test.ts +41 -0
- package/src/pacer.ts +80 -0
- package/src/pacer_internal.ts +21 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type PacerEntry } from "../pacer_internal.js";
|
|
1
2
|
import type { PropertyConfiguration, PropertySchema, Schema } from "../schema.js";
|
|
2
3
|
import type { Icon, PeopleValue, PlaceValue, RelationValue, Schedule, SyncSchedule, TextValue } from "../types.js";
|
|
3
4
|
import type { CapabilityContext } from "./context.js";
|
|
@@ -118,13 +119,13 @@ export type SyncConfiguration<PK extends string, S extends Schema<PK>, State = u
|
|
|
118
119
|
mode?: SyncMode;
|
|
119
120
|
/**
|
|
120
121
|
* How often the sync should run.
|
|
121
|
-
* - "continuous": Run as frequently as the system allows
|
|
122
|
+
* - "continuous": Run as frequently as the system allows
|
|
122
123
|
* - Interval string: Run at specified intervals, e.g. "1h", "30m", "1d"
|
|
123
124
|
*
|
|
124
125
|
* Minimum interval: 1 minute ("1m")
|
|
125
126
|
* Maximum interval: 7 days ("7d")
|
|
126
127
|
*
|
|
127
|
-
* @default "
|
|
128
|
+
* @default "30m"
|
|
128
129
|
*/
|
|
129
130
|
schedule?: Schedule;
|
|
130
131
|
/**
|
|
@@ -153,6 +154,8 @@ type RuntimeContext<UserContext = unknown> = {
|
|
|
153
154
|
state?: UserContext;
|
|
154
155
|
/** Legacy field for user-defined/-controlled state. */
|
|
155
156
|
userContext?: UserContext;
|
|
157
|
+
/** Pacer state from the server for rate limiting. */
|
|
158
|
+
pacers?: Record<string, PacerEntry>;
|
|
156
159
|
};
|
|
157
160
|
/**
|
|
158
161
|
* Creates a special handler for syncing third-party data to a collection.
|
|
@@ -174,6 +177,7 @@ export declare function createSyncCapability<PK extends string, S extends Schema
|
|
|
174
177
|
changes: SyncChange<PK, PropertySchema<PK>>[];
|
|
175
178
|
hasMore: boolean;
|
|
176
179
|
nextUserContext: Context | undefined;
|
|
180
|
+
nextPacerState: Record<string, PacerEntry>;
|
|
177
181
|
}>;
|
|
178
182
|
};
|
|
179
183
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../../src/capabilities/sync.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../../src/capabilities/sync.ts"],"names":[],"mappings":"AACA,OAAO,EAEN,KAAK,UAAU,EAGf,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EACX,qBAAqB,EACrB,cAAc,EACd,MAAM,EACN,MAAM,cAAc,CAAC;AACtB,OAAO,KAAK,EACX,IAAI,EACJ,WAAW,EACX,UAAU,EACV,aAAa,EACb,QAAQ,EACR,YAAY,EACZ,SAAS,EAET,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAGtD;;GAEG;AACH,KAAK,iBAAiB,CAAC,CAAC,SAAS,qBAAqB,IAAI,CAAC,SAAS;IACnE,IAAI,EAAE,QAAQ,CAAC;CACf,GACE,WAAW,GACX,CAAC,SAAS;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,GAC1B,UAAU,GACV,CAAC,SAAS;IAAE,IAAI,EAAE,UAAU,CAAA;CAAE,GAC7B,aAAa,GACb,SAAS,CAAC;AAEf;;GAEG;AACH,MAAM,MAAM,QAAQ,GAAG,SAAS,GAAG,aAAa,CAAC;AAEjD;;GAEG;AACH,MAAM,MAAM,gBAAgB,CAC3B,EAAE,SAAS,MAAM,EACjB,CAAC,SAAS,cAAc,CAAC,EAAE,CAAC,IACzB;IACH;;OAEG;IACH,IAAI,EAAE,QAAQ,CAAC;IACf;;;OAGG;IACH,GAAG,EAAE,MAAM,CAAC;IACZ;;;;OAIG;IACH,UAAU,EAAE;SACV,QAAQ,IAAI,MAAM,CAAC,GAAG,iBAAiB,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;KACrD,CAAC;IACF;;;OAGG;IACH,IAAI,CAAC,EAAE,IAAI,CAAC;IACZ;;;OAGG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC7B,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC9B;;OAEG;IACH,IAAI,EAAE,QAAQ,CAAC;IACf;;;OAGG;IACH,GAAG,EAAE,MAAM,CAAC;CACZ,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC,SAAS,cAAc,CAAC,EAAE,CAAC,IACnE,gBAAgB,CAAC,EAAE,EAAE,CAAC,CAAC,GACvB,gBAAgB,CAAC;AAEpB;;GAEG;AACH,MAAM,MAAM,mBAAmB,CAAC,EAAE,SAAS,MAAM,EAAE,KAAK,GAAG,OAAO,IAAI;IACrE;;;OAGG;IACH,OAAO,EAAE,UAAU,CAAC,EAAE,EAAE,cAAc,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC;IAE9C;;;;OAIG;IACH,OAAO,EAAE,OAAO,CAAC;IAEjB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,KAAK,CAAC;CAClB,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,iBAAiB,CAC5B,EAAE,SAAS,MAAM,EACjB,CAAC,SAAS,MAAM,CAAC,EAAE,CAAC,EACpB,KAAK,GAAG,OAAO,IACZ;IACH;;;;OAIG;IACH,kBAAkB,EAAE,EAAE,CAAC;IAEvB;;OAEG;IACH,MAAM,EAAE,CAAC,CAAC;IAEV;;;;;;;;;OASG;IACH,IAAI,CAAC,EAAE,QAAQ,CAAC;IAEhB;;;;;;;;;OASG;IACH,QAAQ,CAAC,EAAE,QAAQ,CAAC;IAEpB;;;;;;;;;;;;;;OAcG;IACH,OAAO,EAAE,CACR,KAAK,EAAE,KAAK,GAAG,SAAS,EACxB,OAAO,EAAE,iBAAiB,KACtB,OAAO,CAAC,mBAAmB,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC,CAAC;CAC7C,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG,UAAU,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAErE;;GAEG;AACH,KAAK,cAAc,CAAC,WAAW,GAAG,OAAO,IAAI;IAC5C,0EAA0E;IAC1E,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,uDAAuD;IACvD,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,qDAAqD;IACrD,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;CACpC,CAAC;AAEF;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CACnC,EAAE,SAAS,MAAM,EACjB,CAAC,SAAS,MAAM,CAAC,EAAE,CAAC,EACpB,OAAO,GAAG,OAAO,EAChB,GAAG,EAAE,MAAM,EAAE,iBAAiB,EAAE,iBAAiB,CAAC,EAAE,EAAE,CAAC,EAAE,OAAO,CAAC;;;;;;;;;6BAUlC,cAAc,CAAC,OAAO,CAAC;;;;;;EA+BvD"}
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { ExecutionError, unreachable } from "../error.js";
|
|
2
|
+
import {
|
|
3
|
+
getPacerState,
|
|
4
|
+
setPacerState
|
|
5
|
+
} from "../pacer_internal.js";
|
|
2
6
|
import { createCapabilityContext } from "./context.js";
|
|
3
7
|
function createSyncCapability(key, syncConfiguration) {
|
|
4
8
|
return {
|
|
@@ -13,13 +17,19 @@ function createSyncCapability(key, syncConfiguration) {
|
|
|
13
17
|
async handler(runtimeContext) {
|
|
14
18
|
const capabilityContext = createCapabilityContext();
|
|
15
19
|
const state = runtimeContext?.state ?? runtimeContext?.userContext;
|
|
20
|
+
const pacerState = {
|
|
21
|
+
pacers: runtimeContext?.pacers ?? {}
|
|
22
|
+
};
|
|
23
|
+
setPacerState(pacerState);
|
|
16
24
|
const executionResult = await syncConfiguration.execute(state, capabilityContext).catch((err) => {
|
|
17
25
|
throw new ExecutionError(err);
|
|
18
26
|
});
|
|
27
|
+
const updatedPacerState = getPacerState();
|
|
19
28
|
const result = {
|
|
20
29
|
changes: executionResult.changes,
|
|
21
30
|
hasMore: executionResult.hasMore,
|
|
22
|
-
nextUserContext: executionResult.nextState
|
|
31
|
+
nextUserContext: executionResult.nextState,
|
|
32
|
+
nextPacerState: updatedPacerState.pacers
|
|
23
33
|
};
|
|
24
34
|
process.stdout.write(`
|
|
25
35
|
<output>${JSON.stringify(result)}</output>
|
|
@@ -33,10 +43,14 @@ const MS_PER_HOUR = 60 * MS_PER_MINUTE;
|
|
|
33
43
|
const MS_PER_DAY = 24 * MS_PER_HOUR;
|
|
34
44
|
const MIN_INTERVAL_MS = MS_PER_MINUTE;
|
|
35
45
|
const MAX_INTERVAL_MS = 7 * MS_PER_DAY;
|
|
46
|
+
const DEFAULT_INTERVAL_MS = 30 * MS_PER_MINUTE;
|
|
36
47
|
function parseSchedule(schedule) {
|
|
37
|
-
if (
|
|
48
|
+
if (schedule === "continuous") {
|
|
38
49
|
return { type: "continuous" };
|
|
39
50
|
}
|
|
51
|
+
if (!schedule) {
|
|
52
|
+
return { type: "interval", intervalMs: DEFAULT_INTERVAL_MS };
|
|
53
|
+
}
|
|
40
54
|
const match = schedule.match(/^(\d+)(m|h|d)$/);
|
|
41
55
|
if (!match || !match[1] || !match[2]) {
|
|
42
56
|
throw new Error(
|
package/dist/index.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ export type { CapabilityContext } from "./capabilities/context.js";
|
|
|
4
4
|
export type { NotionManagedOAuthConfiguration, OAuthCapability, OAuthConfiguration, UserManagedOAuthConfiguration, } from "./capabilities/oauth.js";
|
|
5
5
|
export type { SyncCapability, SyncChange, SyncChangeDelete, SyncChangeUpsert, SyncConfiguration, SyncExecutionResult, SyncMode, } from "./capabilities/sync.js";
|
|
6
6
|
export type { ToolCapability, ToolConfiguration } from "./capabilities/tool.js";
|
|
7
|
+
export { Pacer } from "./pacer.js";
|
|
7
8
|
export type { Icon, ImageIcon, NoticonColor, NoticonName, PlaceValue, Schedule, } from "./types.js";
|
|
8
9
|
export { Worker } from "./worker.js";
|
|
9
10
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,cAAc,CAAC;AACvE,YAAY,EACX,oBAAoB,EACpB,uBAAuB,EACvB,eAAe,EACf,kBAAkB,GAClB,MAAM,8BAA8B,CAAC;AACtC,YAAY,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AACnE,YAAY,EACX,+BAA+B,EAC/B,eAAe,EACf,kBAAkB,EAClB,6BAA6B,GAC7B,MAAM,yBAAyB,CAAC;AACjC,YAAY,EACX,cAAc,EACd,UAAU,EACV,gBAAgB,EAChB,gBAAgB,EAChB,iBAAiB,EACjB,mBAAmB,EACnB,QAAQ,GACR,MAAM,wBAAwB,CAAC;AAChC,YAAY,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,cAAc,CAAC;AACvE,YAAY,EACX,oBAAoB,EACpB,uBAAuB,EACvB,eAAe,EACf,kBAAkB,GAClB,MAAM,8BAA8B,CAAC;AACtC,YAAY,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AACnE,YAAY,EACX,+BAA+B,EAC/B,eAAe,EACf,kBAAkB,EAClB,6BAA6B,GAC7B,MAAM,yBAAyB,CAAC;AACjC,YAAY,EACX,cAAc,EACd,UAAU,EACV,gBAAgB,EAChB,gBAAgB,EAChB,iBAAiB,EACjB,mBAAmB,EACnB,QAAQ,GACR,MAAM,wBAAwB,CAAC;AAChC,YAAY,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAEhF,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,YAAY,EACX,IAAI,EACJ,SAAS,EACT,YAAY,EACZ,WAAW,EACX,UAAU,EACV,QAAQ,GACR,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC"}
|
package/dist/index.js
CHANGED
package/dist/pacer.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pacer module for rate limiting API requests.
|
|
3
|
+
*
|
|
4
|
+
* The Pacer ensures requests are evenly spaced over time to respect third-party
|
|
5
|
+
* API rate limits. Instead of making all requests immediately until hitting a 429,
|
|
6
|
+
* Pacer paces requests throughout the rate limit window.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* import { Pacer } from "@project-ajax/sdk/pacer"
|
|
11
|
+
*
|
|
12
|
+
* // Rate limit: 10 requests per minute
|
|
13
|
+
* await Pacer.wait("salesforce-api", { requests: 10, intervalMs: 60 * 1000 })
|
|
14
|
+
*
|
|
15
|
+
* // Now make your API call
|
|
16
|
+
* const data = await fetchFromSalesforce()
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* @module
|
|
20
|
+
*/
|
|
21
|
+
export type PacerLimit = {
|
|
22
|
+
requests: number;
|
|
23
|
+
intervalMs: number;
|
|
24
|
+
};
|
|
25
|
+
export declare const Pacer: {
|
|
26
|
+
/**
|
|
27
|
+
* Wait until a request can proceed under the specified rate limit.
|
|
28
|
+
*
|
|
29
|
+
* This function paces requests evenly across the rate limit interval. For example,
|
|
30
|
+
* with 60 requests allowed per hour, requests are spaced approximately 1 minute apart.
|
|
31
|
+
*
|
|
32
|
+
* @param key - Unique identifier for this rate limit scope (e.g., "salesforce-api", "jira:token123"). If you're not sure, use any string.
|
|
33
|
+
* @param limit - Rate limit configuration: `requests` per `intervalMs`.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```ts
|
|
37
|
+
* // Salesforce: 30,000 requests per 24 hours
|
|
38
|
+
* await Pacer.wait("salesforce", { requests: 30_000, intervalMs: 24 * 60 * 60 * 1000 })
|
|
39
|
+
*
|
|
40
|
+
* // Jira: 100 requests per minute
|
|
41
|
+
* await Pacer.wait("jira", { requests: 100, intervalMs: 60 * 1000 })
|
|
42
|
+
*
|
|
43
|
+
* // Multiple rate limits for different endpoints
|
|
44
|
+
* await Pacer.wait("api-read", { requests: 1000, intervalMs: 60 * 1000 })
|
|
45
|
+
* await Pacer.wait("api-write", { requests: 100, intervalMs: 60 * 1000 })
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
wait(key: string, limit: PacerLimit): Promise<void>;
|
|
49
|
+
};
|
|
50
|
+
//# sourceMappingURL=pacer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pacer.d.ts","sourceRoot":"","sources":["../src/pacer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAIH,MAAM,MAAM,UAAU,GAAG;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,eAAO,MAAM,KAAK;IACjB;;;;;;;;;;;;;;;;;;;;;OAqBG;cACa,MAAM,SAAS,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;CA4BzD,CAAC"}
|
package/dist/pacer.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { getPacerState, setPacerState } from "./pacer_internal.js";
|
|
2
|
+
const Pacer = {
|
|
3
|
+
/**
|
|
4
|
+
* Wait until a request can proceed under the specified rate limit.
|
|
5
|
+
*
|
|
6
|
+
* This function paces requests evenly across the rate limit interval. For example,
|
|
7
|
+
* with 60 requests allowed per hour, requests are spaced approximately 1 minute apart.
|
|
8
|
+
*
|
|
9
|
+
* @param key - Unique identifier for this rate limit scope (e.g., "salesforce-api", "jira:token123"). If you're not sure, use any string.
|
|
10
|
+
* @param limit - Rate limit configuration: `requests` per `intervalMs`.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* // Salesforce: 30,000 requests per 24 hours
|
|
15
|
+
* await Pacer.wait("salesforce", { requests: 30_000, intervalMs: 24 * 60 * 60 * 1000 })
|
|
16
|
+
*
|
|
17
|
+
* // Jira: 100 requests per minute
|
|
18
|
+
* await Pacer.wait("jira", { requests: 100, intervalMs: 60 * 1000 })
|
|
19
|
+
*
|
|
20
|
+
* // Multiple rate limits for different endpoints
|
|
21
|
+
* await Pacer.wait("api-read", { requests: 1000, intervalMs: 60 * 1000 })
|
|
22
|
+
* await Pacer.wait("api-write", { requests: 100, intervalMs: 60 * 1000 })
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
async wait(key, limit) {
|
|
26
|
+
if (limit.requests <= 0) {
|
|
27
|
+
throw new Error("requests must be greater than 0");
|
|
28
|
+
}
|
|
29
|
+
if (limit.intervalMs <= 0) {
|
|
30
|
+
throw new Error("intervalMs must be greater than 0");
|
|
31
|
+
}
|
|
32
|
+
const now = Date.now();
|
|
33
|
+
const paceMs = Math.ceil(limit.intervalMs / limit.requests);
|
|
34
|
+
const state = getPacerState();
|
|
35
|
+
const existing = state.pacers[key];
|
|
36
|
+
const lastScheduledAtMs = existing?.lastScheduledAtMs ?? 0;
|
|
37
|
+
const scheduledAtMs = Math.max(lastScheduledAtMs + paceMs, now);
|
|
38
|
+
const delayMs = scheduledAtMs - now;
|
|
39
|
+
state.pacers[key] = { lastScheduledAtMs: scheduledAtMs };
|
|
40
|
+
setPacerState(state);
|
|
41
|
+
if (delayMs > 0) {
|
|
42
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
export {
|
|
47
|
+
Pacer
|
|
48
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pacer.test.d.ts","sourceRoot":"","sources":["../src/pacer.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pacer_internal.d.ts","sourceRoot":"","sources":["../src/pacer_internal.ts"],"names":[],"mappings":"AAQA,MAAM,MAAM,UAAU,GAAG;IACxB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;CACnC,CAAC;AAIF,wBAAgB,aAAa,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,CAErD;AAED,wBAAgB,aAAa,IAAI,UAAU,CAE1C"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@project-ajax/sdk",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.76",
|
|
4
4
|
"description": "An SDK for building workers for the Project Ajax platform",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
@@ -28,6 +28,10 @@
|
|
|
28
28
|
"./types": {
|
|
29
29
|
"types": "./dist/types.d.ts",
|
|
30
30
|
"default": "./dist/types.js"
|
|
31
|
+
},
|
|
32
|
+
"./pacer": {
|
|
33
|
+
"types": "./dist/pacer.d.ts",
|
|
34
|
+
"default": "./dist/pacer.js"
|
|
31
35
|
}
|
|
32
36
|
},
|
|
33
37
|
"engines": {
|
package/src/capabilities/sync.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import { ExecutionError, unreachable } from "../error.js";
|
|
2
|
+
import {
|
|
3
|
+
getPacerState,
|
|
4
|
+
type PacerEntry,
|
|
5
|
+
type PacerState,
|
|
6
|
+
setPacerState,
|
|
7
|
+
} from "../pacer_internal.js";
|
|
2
8
|
import type {
|
|
3
9
|
PropertyConfiguration,
|
|
4
10
|
PropertySchema,
|
|
@@ -156,13 +162,13 @@ export type SyncConfiguration<
|
|
|
156
162
|
|
|
157
163
|
/**
|
|
158
164
|
* How often the sync should run.
|
|
159
|
-
* - "continuous": Run as frequently as the system allows
|
|
165
|
+
* - "continuous": Run as frequently as the system allows
|
|
160
166
|
* - Interval string: Run at specified intervals, e.g. "1h", "30m", "1d"
|
|
161
167
|
*
|
|
162
168
|
* Minimum interval: 1 minute ("1m")
|
|
163
169
|
* Maximum interval: 7 days ("7d")
|
|
164
170
|
*
|
|
165
|
-
* @default "
|
|
171
|
+
* @default "30m"
|
|
166
172
|
*/
|
|
167
173
|
schedule?: Schedule;
|
|
168
174
|
|
|
@@ -197,6 +203,8 @@ type RuntimeContext<UserContext = unknown> = {
|
|
|
197
203
|
state?: UserContext;
|
|
198
204
|
/** Legacy field for user-defined/-controlled state. */
|
|
199
205
|
userContext?: UserContext;
|
|
206
|
+
/** Pacer state from the server for rate limiting. */
|
|
207
|
+
pacers?: Record<string, PacerEntry>;
|
|
200
208
|
};
|
|
201
209
|
|
|
202
210
|
/**
|
|
@@ -223,16 +231,27 @@ export function createSyncCapability<
|
|
|
223
231
|
async handler(runtimeContext?: RuntimeContext<Context>) {
|
|
224
232
|
const capabilityContext = createCapabilityContext();
|
|
225
233
|
const state = runtimeContext?.state ?? runtimeContext?.userContext;
|
|
234
|
+
|
|
235
|
+
// Initialize pacer state from runtime context
|
|
236
|
+
const pacerState: PacerState = {
|
|
237
|
+
pacers: runtimeContext?.pacers ?? {},
|
|
238
|
+
};
|
|
239
|
+
setPacerState(pacerState);
|
|
240
|
+
|
|
226
241
|
const executionResult = await syncConfiguration
|
|
227
242
|
.execute(state, capabilityContext)
|
|
228
243
|
.catch((err) => {
|
|
229
244
|
throw new ExecutionError(err);
|
|
230
245
|
});
|
|
231
246
|
|
|
247
|
+
// Get updated pacer state after execution
|
|
248
|
+
const updatedPacerState = getPacerState();
|
|
249
|
+
|
|
232
250
|
const result = {
|
|
233
251
|
changes: executionResult.changes,
|
|
234
252
|
hasMore: executionResult.hasMore,
|
|
235
253
|
nextUserContext: executionResult.nextState,
|
|
254
|
+
nextPacerState: updatedPacerState.pacers,
|
|
236
255
|
};
|
|
237
256
|
|
|
238
257
|
process.stdout.write(`\n<output>${JSON.stringify(result)}</output>\n`);
|
|
@@ -249,14 +268,20 @@ const MS_PER_DAY = 24 * MS_PER_HOUR;
|
|
|
249
268
|
const MIN_INTERVAL_MS = MS_PER_MINUTE; // 1m
|
|
250
269
|
const MAX_INTERVAL_MS = 7 * MS_PER_DAY; // 7d
|
|
251
270
|
|
|
271
|
+
const DEFAULT_INTERVAL_MS = 30 * MS_PER_MINUTE; // 30m
|
|
272
|
+
|
|
252
273
|
/**
|
|
253
274
|
* Parses a user-friendly schedule string into the normalized backend format.
|
|
254
275
|
*/
|
|
255
276
|
function parseSchedule(schedule: Schedule | undefined): SyncSchedule {
|
|
256
|
-
if (
|
|
277
|
+
if (schedule === "continuous") {
|
|
257
278
|
return { type: "continuous" };
|
|
258
279
|
}
|
|
259
280
|
|
|
281
|
+
if (!schedule) {
|
|
282
|
+
return { type: "interval", intervalMs: DEFAULT_INTERVAL_MS };
|
|
283
|
+
}
|
|
284
|
+
|
|
260
285
|
const match = schedule.match(/^(\d+)(m|h|d)$/);
|
|
261
286
|
if (!match || !match[1] || !match[2]) {
|
|
262
287
|
throw new Error(
|
package/src/index.ts
CHANGED
|
@@ -22,6 +22,8 @@ export type {
|
|
|
22
22
|
SyncMode,
|
|
23
23
|
} from "./capabilities/sync.js";
|
|
24
24
|
export type { ToolCapability, ToolConfiguration } from "./capabilities/tool.js";
|
|
25
|
+
// Pacer module re-exported for convenience (users can also import from "@project-ajax/sdk/pacer")
|
|
26
|
+
export { Pacer } from "./pacer.js";
|
|
25
27
|
export type {
|
|
26
28
|
Icon,
|
|
27
29
|
ImageIcon,
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { Pacer } from "./pacer.js";
|
|
4
|
+
import { getPacerState, setPacerState } from "./pacer_internal.js";
|
|
5
|
+
|
|
6
|
+
describe("Pacer.wait", () => {
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
vi.useFakeTimers();
|
|
9
|
+
vi.setSystemTime(0);
|
|
10
|
+
setPacerState({ pacers: {} });
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
vi.useRealTimers();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("paces requests based on the rate limit", async () => {
|
|
18
|
+
const first = Pacer.wait("api", { requests: 10, intervalMs: 60_000 });
|
|
19
|
+
|
|
20
|
+
expect(getPacerState().pacers.api?.lastScheduledAtMs).toBe(6000);
|
|
21
|
+
await vi.advanceTimersByTimeAsync(6000);
|
|
22
|
+
await first;
|
|
23
|
+
|
|
24
|
+
const second = Pacer.wait("api", { requests: 10, intervalMs: 60_000 });
|
|
25
|
+
expect(getPacerState().pacers.api?.lastScheduledAtMs).toBe(12_000);
|
|
26
|
+
await vi.advanceTimersByTimeAsync(6000);
|
|
27
|
+
await second;
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("does not wait when the next slot is already in the past", async () => {
|
|
31
|
+
const first = Pacer.wait("api", { requests: 10, intervalMs: 60_000 });
|
|
32
|
+
await vi.advanceTimersByTimeAsync(6000);
|
|
33
|
+
await first;
|
|
34
|
+
|
|
35
|
+
vi.advanceTimersByTime(20_000);
|
|
36
|
+
|
|
37
|
+
const second = Pacer.wait("api", { requests: 10, intervalMs: 60_000 });
|
|
38
|
+
expect(getPacerState().pacers.api?.lastScheduledAtMs).toBe(26_000);
|
|
39
|
+
await second;
|
|
40
|
+
});
|
|
41
|
+
});
|
package/src/pacer.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pacer module for rate limiting API requests.
|
|
3
|
+
*
|
|
4
|
+
* The Pacer ensures requests are evenly spaced over time to respect third-party
|
|
5
|
+
* API rate limits. Instead of making all requests immediately until hitting a 429,
|
|
6
|
+
* Pacer paces requests throughout the rate limit window.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* import { Pacer } from "@project-ajax/sdk/pacer"
|
|
11
|
+
*
|
|
12
|
+
* // Rate limit: 10 requests per minute
|
|
13
|
+
* await Pacer.wait("salesforce-api", { requests: 10, intervalMs: 60 * 1000 })
|
|
14
|
+
*
|
|
15
|
+
* // Now make your API call
|
|
16
|
+
* const data = await fetchFromSalesforce()
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* @module
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { getPacerState, setPacerState } from "./pacer_internal.js";
|
|
23
|
+
|
|
24
|
+
export type PacerLimit = {
|
|
25
|
+
requests: number;
|
|
26
|
+
intervalMs: number;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const Pacer = {
|
|
30
|
+
/**
|
|
31
|
+
* Wait until a request can proceed under the specified rate limit.
|
|
32
|
+
*
|
|
33
|
+
* This function paces requests evenly across the rate limit interval. For example,
|
|
34
|
+
* with 60 requests allowed per hour, requests are spaced approximately 1 minute apart.
|
|
35
|
+
*
|
|
36
|
+
* @param key - Unique identifier for this rate limit scope (e.g., "salesforce-api", "jira:token123"). If you're not sure, use any string.
|
|
37
|
+
* @param limit - Rate limit configuration: `requests` per `intervalMs`.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```ts
|
|
41
|
+
* // Salesforce: 30,000 requests per 24 hours
|
|
42
|
+
* await Pacer.wait("salesforce", { requests: 30_000, intervalMs: 24 * 60 * 60 * 1000 })
|
|
43
|
+
*
|
|
44
|
+
* // Jira: 100 requests per minute
|
|
45
|
+
* await Pacer.wait("jira", { requests: 100, intervalMs: 60 * 1000 })
|
|
46
|
+
*
|
|
47
|
+
* // Multiple rate limits for different endpoints
|
|
48
|
+
* await Pacer.wait("api-read", { requests: 1000, intervalMs: 60 * 1000 })
|
|
49
|
+
* await Pacer.wait("api-write", { requests: 100, intervalMs: 60 * 1000 })
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
async wait(key: string, limit: PacerLimit): Promise<void> {
|
|
53
|
+
if (limit.requests <= 0) {
|
|
54
|
+
throw new Error("requests must be greater than 0");
|
|
55
|
+
}
|
|
56
|
+
if (limit.intervalMs <= 0) {
|
|
57
|
+
throw new Error("intervalMs must be greater than 0");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const now = Date.now();
|
|
61
|
+
const paceMs = Math.ceil(limit.intervalMs / limit.requests);
|
|
62
|
+
|
|
63
|
+
const state = getPacerState();
|
|
64
|
+
const existing = state.pacers[key];
|
|
65
|
+
const lastScheduledAtMs = existing?.lastScheduledAtMs ?? 0;
|
|
66
|
+
|
|
67
|
+
// Schedule at the later of: (last scheduled + pace) or now
|
|
68
|
+
const scheduledAtMs = Math.max(lastScheduledAtMs + paceMs, now);
|
|
69
|
+
const delayMs = scheduledAtMs - now;
|
|
70
|
+
|
|
71
|
+
// Update state with new scheduled timestamp
|
|
72
|
+
state.pacers[key] = { lastScheduledAtMs: scheduledAtMs };
|
|
73
|
+
setPacerState(state);
|
|
74
|
+
|
|
75
|
+
// Sleep if needed
|
|
76
|
+
if (delayMs > 0) {
|
|
77
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal pacer state helpers used by the runtime.
|
|
3
|
+
* @internal
|
|
4
|
+
*/
|
|
5
|
+
export type PacerEntry = {
|
|
6
|
+
lastScheduledAtMs: number;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type PacerState = {
|
|
10
|
+
pacers: Record<string, PacerEntry>;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
let pacerState: PacerState = { pacers: {} };
|
|
14
|
+
|
|
15
|
+
export function setPacerState(state: PacerState): void {
|
|
16
|
+
pacerState = state;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function getPacerState(): PacerState {
|
|
20
|
+
return pacerState;
|
|
21
|
+
}
|