@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.
@@ -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 (default)
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 "continuous"
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":"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"}
@@ -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 (!schedule || schedule === "continuous") {
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
@@ -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;AAChF,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"}
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
@@ -1,6 +1,8 @@
1
1
  import { emojiIcon, imageIcon, notionIcon, place } from "./builder.js";
2
+ import { Pacer } from "./pacer.js";
2
3
  import { Worker } from "./worker.js";
3
4
  export {
5
+ Pacer,
4
6
  Worker,
5
7
  emojiIcon,
6
8
  imageIcon,
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=pacer.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pacer.test.d.ts","sourceRoot":"","sources":["../src/pacer.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,6 @@
1
+ export type PacerState = {
2
+ pacers: Record<string, PacerEntry>;
3
+ };
4
+ export declare function setPacerState(state: PacerState): void;
5
+ export declare function getPacerState(): PacerState;
6
+ //# sourceMappingURL=pacer_internal.d.ts.map
@@ -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"}
@@ -0,0 +1,11 @@
1
+ let pacerState = { pacers: {} };
2
+ function setPacerState(state) {
3
+ pacerState = state;
4
+ }
5
+ function getPacerState() {
6
+ return pacerState;
7
+ }
8
+ export {
9
+ getPacerState,
10
+ setPacerState
11
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@project-ajax/sdk",
3
- "version": "0.0.75",
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": {
@@ -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 (default)
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 "continuous"
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 (!schedule || schedule === "continuous") {
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
+ }