@notionhq/workers 0.0.86 → 0.2.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/dist/ajv-formats.d.js +0 -0
- package/dist/capabilities/automation.js +2 -2
- package/dist/capabilities/sync.d.ts +73 -16
- package/dist/capabilities/sync.d.ts.map +1 -1
- package/dist/capabilities/sync.js +19 -10
- package/dist/capabilities/tool.d.ts.map +1 -1
- package/dist/capabilities/tool.js +18 -10
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/pacer.d.ts +24 -0
- package/dist/pacer.d.ts.map +1 -0
- package/dist/pacer.js +14 -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 +32 -0
- package/dist/schema.d.ts +3 -10
- package/dist/schema.d.ts.map +1 -1
- package/dist/types.d.ts +5 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/worker.d.ts +138 -5
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +73 -7
- package/dist/worker.test.d.ts +2 -0
- package/dist/worker.test.d.ts.map +1 -0
- package/package.json +7 -2
- package/src/ajv-formats.d.ts +7 -0
- package/src/capabilities/automation.test.ts +2 -2
- package/src/capabilities/automation.ts +2 -2
- package/src/capabilities/sync.test.ts +89 -32
- package/src/capabilities/sync.ts +52 -25
- package/src/capabilities/tool.test.ts +47 -3
- package/src/capabilities/tool.ts +12 -4
- package/src/index.ts +9 -0
- package/src/pacer.test.ts +110 -0
- package/src/pacer.ts +25 -0
- package/src/pacer_internal.ts +60 -0
- package/src/schema.ts +3 -10
- package/src/types.ts +4 -2
- package/src/worker.test.ts +167 -0
- package/src/worker.ts +205 -7
package/src/capabilities/sync.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { ExecutionError, unreachable } from "../error.js";
|
|
2
|
+
import {
|
|
3
|
+
getPacerState,
|
|
4
|
+
type PacerEntry,
|
|
5
|
+
setPacerState,
|
|
6
|
+
} from "../pacer_internal.js";
|
|
2
7
|
import type {
|
|
3
8
|
PropertyConfiguration,
|
|
4
9
|
PropertySchema,
|
|
@@ -15,6 +20,7 @@ import type {
|
|
|
15
20
|
TextValue,
|
|
16
21
|
TimeUnit,
|
|
17
22
|
} from "../types.js";
|
|
23
|
+
import type { DatabaseHandle } from "../worker.js";
|
|
18
24
|
import type { CapabilityContext } from "./context.js";
|
|
19
25
|
import { createCapabilityContext } from "./context.js";
|
|
20
26
|
|
|
@@ -49,9 +55,13 @@ export type SyncChangeUpsert<
|
|
|
49
55
|
type: "upsert";
|
|
50
56
|
/**
|
|
51
57
|
* A unique identifier for this record, used to match against existing pages.
|
|
52
|
-
* This value will be stored in the property specified by `primaryKeyProperty`.
|
|
53
58
|
*/
|
|
54
59
|
key: string;
|
|
60
|
+
/**
|
|
61
|
+
* The key of the database to write to. If omitted, defaults to
|
|
62
|
+
* the database associated with the sync capability.
|
|
63
|
+
*/
|
|
64
|
+
targetDatabaseKey?: string;
|
|
55
65
|
/**
|
|
56
66
|
* The property values for this record.
|
|
57
67
|
* Keys must match the property names defined in the schema.
|
|
@@ -60,6 +70,13 @@ export type SyncChangeUpsert<
|
|
|
60
70
|
properties: {
|
|
61
71
|
[Property in keyof S]: PropertyValueType<S[Property]>;
|
|
62
72
|
};
|
|
73
|
+
/**
|
|
74
|
+
* When this record was last updated in the upstream system.
|
|
75
|
+
* ISO 8601 string (e.g. `"2026-03-19T12:00:00Z"`).
|
|
76
|
+
* Used for conflict resolution when multiple syncs write to the same database.
|
|
77
|
+
* Recommended, but optional.
|
|
78
|
+
*/
|
|
79
|
+
upstreamUpdatedAt?: string;
|
|
63
80
|
/**
|
|
64
81
|
* Optional icon to use as the icon for this row's page.
|
|
65
82
|
* Use the `Builder.emojiIcon()`, `Builder.notionIcon()`, or `Builder.imageIcon()` helpers.
|
|
@@ -83,9 +100,13 @@ export type SyncChangeDelete = {
|
|
|
83
100
|
type: "delete";
|
|
84
101
|
/**
|
|
85
102
|
* The unique identifier of the record to delete.
|
|
86
|
-
* Must match the `key` of a previously upserted record.
|
|
87
103
|
*/
|
|
88
104
|
key: string;
|
|
105
|
+
/**
|
|
106
|
+
* The key of the database to delete from. If omitted, defaults to
|
|
107
|
+
* the database associated with the sync capability.
|
|
108
|
+
*/
|
|
109
|
+
targetDatabaseKey?: string;
|
|
89
110
|
};
|
|
90
111
|
|
|
91
112
|
/**
|
|
@@ -123,8 +144,7 @@ export type SyncExecutionResult<PK extends string, State = unknown> = {
|
|
|
123
144
|
};
|
|
124
145
|
|
|
125
146
|
/**
|
|
126
|
-
*
|
|
127
|
-
* source and a third-party source.
|
|
147
|
+
* Configuration for a sync capability that syncs data to a database.
|
|
128
148
|
*/
|
|
129
149
|
export type SyncConfiguration<
|
|
130
150
|
PK extends string,
|
|
@@ -132,16 +152,9 @@ export type SyncConfiguration<
|
|
|
132
152
|
State = unknown,
|
|
133
153
|
> = {
|
|
134
154
|
/**
|
|
135
|
-
* The
|
|
136
|
-
* third-party data. This is used to match existing pages to
|
|
137
|
-
* records in the third-party service. Must be a property defined in the schema.
|
|
155
|
+
* The database to sync data into.
|
|
138
156
|
*/
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* The schema defining the structure of properties in the collection.
|
|
143
|
-
*/
|
|
144
|
-
schema: S;
|
|
157
|
+
database: DatabaseHandle<PK, S>;
|
|
145
158
|
|
|
146
159
|
/**
|
|
147
160
|
* How the sync handles data lifecycle:
|
|
@@ -158,6 +171,7 @@ export type SyncConfiguration<
|
|
|
158
171
|
/**
|
|
159
172
|
* How often the sync should run.
|
|
160
173
|
* - "continuous": Run as frequently as the system allows
|
|
174
|
+
* - "manual": Only run when explicitly triggered
|
|
161
175
|
* - Interval string: Run at specified intervals, e.g. "1h", "30m", "1d"
|
|
162
176
|
*
|
|
163
177
|
* Minimum interval: 1 minute ("1m")
|
|
@@ -198,10 +212,12 @@ type RuntimeContext<UserContext = unknown> = {
|
|
|
198
212
|
state?: UserContext;
|
|
199
213
|
/** Legacy field for user-defined/-controlled state. */
|
|
200
214
|
userContext?: UserContext;
|
|
215
|
+
/** Pacer state from the server for rate limiting. */
|
|
216
|
+
pacers?: Record<string, PacerEntry>;
|
|
201
217
|
};
|
|
202
218
|
|
|
203
219
|
/**
|
|
204
|
-
* Creates a special handler for syncing third-party data to a
|
|
220
|
+
* Creates a special handler for syncing third-party data to a database.
|
|
205
221
|
*
|
|
206
222
|
* @param syncConfiguration - The configuration for the sync.
|
|
207
223
|
* @returns A handler function that executes the sync function, and passes data
|
|
@@ -216,10 +232,12 @@ export function createSyncCapability<
|
|
|
216
232
|
_tag: "sync" as const,
|
|
217
233
|
key,
|
|
218
234
|
config: {
|
|
219
|
-
|
|
220
|
-
|
|
235
|
+
databaseKey: syncConfiguration.database.key,
|
|
236
|
+
primaryKeyProperty: syncConfiguration.database.config.primaryKeyProperty,
|
|
221
237
|
mode: syncConfiguration.mode,
|
|
222
|
-
schedule:
|
|
238
|
+
schedule: syncConfiguration.schedule
|
|
239
|
+
? parseSchedule(syncConfiguration.schedule)
|
|
240
|
+
: undefined,
|
|
223
241
|
},
|
|
224
242
|
async handler(
|
|
225
243
|
runtimeContext?: RuntimeContext<Context>,
|
|
@@ -227,6 +245,10 @@ export function createSyncCapability<
|
|
|
227
245
|
) {
|
|
228
246
|
const capabilityContext = createCapabilityContext();
|
|
229
247
|
const state = runtimeContext?.state ?? runtimeContext?.userContext;
|
|
248
|
+
|
|
249
|
+
// Initialize pacer state from runtime context
|
|
250
|
+
setPacerState({ pacers: runtimeContext?.pacers ?? {} });
|
|
251
|
+
|
|
230
252
|
let executionResult: SyncExecutionResult<PK, Context>;
|
|
231
253
|
try {
|
|
232
254
|
executionResult = await syncConfiguration.execute(
|
|
@@ -237,23 +259,30 @@ export function createSyncCapability<
|
|
|
237
259
|
const error = new ExecutionError(err);
|
|
238
260
|
if (!options?.concreteOutput) {
|
|
239
261
|
process.stdout.write(
|
|
240
|
-
`\n<
|
|
262
|
+
`\n<__notion_output__>${JSON.stringify({ _tag: "error", error: { name: error.name, message: error.message } })}</__notion_output__>\n`,
|
|
241
263
|
);
|
|
242
264
|
}
|
|
243
265
|
throw error;
|
|
244
266
|
}
|
|
245
267
|
|
|
268
|
+
const changes = executionResult.changes.map((change) => ({
|
|
269
|
+
...change,
|
|
270
|
+
targetDatabaseKey:
|
|
271
|
+
change.targetDatabaseKey ?? syncConfiguration.database.key,
|
|
272
|
+
}));
|
|
273
|
+
|
|
246
274
|
const result = {
|
|
247
|
-
changes
|
|
275
|
+
changes,
|
|
248
276
|
hasMore: executionResult.hasMore,
|
|
249
277
|
nextUserContext: executionResult.nextState,
|
|
278
|
+
nextPacerStates: getPacerState().pacers,
|
|
250
279
|
};
|
|
251
280
|
|
|
252
281
|
if (options?.concreteOutput) {
|
|
253
282
|
return result;
|
|
254
283
|
} else {
|
|
255
284
|
process.stdout.write(
|
|
256
|
-
`\n<
|
|
285
|
+
`\n<__notion_output__>${JSON.stringify({ _tag: "success", value: result })}</__notion_output__>\n`,
|
|
257
286
|
);
|
|
258
287
|
}
|
|
259
288
|
|
|
@@ -269,18 +298,16 @@ const MS_PER_DAY = 24 * MS_PER_HOUR;
|
|
|
269
298
|
const MIN_INTERVAL_MS = MS_PER_MINUTE; // 1m
|
|
270
299
|
const MAX_INTERVAL_MS = 7 * MS_PER_DAY; // 7d
|
|
271
300
|
|
|
272
|
-
const DEFAULT_INTERVAL_MS = 30 * MS_PER_MINUTE; // 30m
|
|
273
|
-
|
|
274
301
|
/**
|
|
275
302
|
* Parses a user-friendly schedule string into the normalized backend format.
|
|
276
303
|
*/
|
|
277
|
-
function parseSchedule(schedule: Schedule
|
|
304
|
+
function parseSchedule(schedule: Schedule): SyncSchedule {
|
|
278
305
|
if (schedule === "continuous") {
|
|
279
306
|
return { type: "continuous" };
|
|
280
307
|
}
|
|
281
308
|
|
|
282
|
-
if (
|
|
283
|
-
return { type: "
|
|
309
|
+
if (schedule === "manual") {
|
|
310
|
+
return { type: "manual" };
|
|
284
311
|
}
|
|
285
312
|
|
|
286
313
|
const match = schedule.match(/^(\d+)(m|h|d)$/);
|
|
@@ -41,7 +41,7 @@ describe("Worker.tool", () => {
|
|
|
41
41
|
expect(result).toEqual({ _tag: "success", value: "Hello, Alice!" });
|
|
42
42
|
|
|
43
43
|
expect(stdoutSpy).toHaveBeenCalledWith(
|
|
44
|
-
`\n<
|
|
44
|
+
`\n<__notion_output__>{"_tag":"success","value":"Hello, Alice!"}</__notion_output__>\n`,
|
|
45
45
|
);
|
|
46
46
|
});
|
|
47
47
|
|
|
@@ -68,7 +68,7 @@ describe("Worker.tool", () => {
|
|
|
68
68
|
});
|
|
69
69
|
|
|
70
70
|
expect(stdoutSpy).toHaveBeenCalledWith(
|
|
71
|
-
`\n<
|
|
71
|
+
`\n<__notion_output__>{"_tag":"success","value":{"data":"Data for ID 42"}}</__notion_output__>\n`,
|
|
72
72
|
);
|
|
73
73
|
});
|
|
74
74
|
|
|
@@ -125,7 +125,7 @@ describe("Worker.tool", () => {
|
|
|
125
125
|
|
|
126
126
|
expect(stdoutSpy).toHaveBeenCalledWith(
|
|
127
127
|
expect.stringContaining(
|
|
128
|
-
`\n<
|
|
128
|
+
`\n<__notion_output__>{"_tag":"error","error":{"name":"InvalidToolInputError"`,
|
|
129
129
|
),
|
|
130
130
|
);
|
|
131
131
|
});
|
|
@@ -161,6 +161,50 @@ describe("Worker.tool", () => {
|
|
|
161
161
|
);
|
|
162
162
|
});
|
|
163
163
|
|
|
164
|
+
it("accepts schema with format string types (date, datetime, etc.)", () => {
|
|
165
|
+
const capability = createToolCapability<
|
|
166
|
+
{ due: string; createdAt: string },
|
|
167
|
+
string
|
|
168
|
+
>("dateFormats", {
|
|
169
|
+
title: "Date Formats",
|
|
170
|
+
description: "Uses date and datetime format strings",
|
|
171
|
+
schema: j.object({
|
|
172
|
+
due: j.date(),
|
|
173
|
+
createdAt: j.datetime(),
|
|
174
|
+
}),
|
|
175
|
+
execute: ({ due, createdAt }) => `${due} ${createdAt}`,
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
expect(capability.config.schema).toBeDefined();
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("rejects invalid date-time format at runtime", async () => {
|
|
182
|
+
const capability = createToolCapability<{ ts: string }, string>(
|
|
183
|
+
"datetimeValidation",
|
|
184
|
+
{
|
|
185
|
+
title: "Datetime Validation",
|
|
186
|
+
description: "Validates datetime input",
|
|
187
|
+
schema: j.object({
|
|
188
|
+
ts: j.datetime(),
|
|
189
|
+
}),
|
|
190
|
+
execute: ({ ts }) => ts,
|
|
191
|
+
},
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
const valid = await capability.handler({ ts: "2024-01-01T00:00:00Z" });
|
|
195
|
+
expect(valid).toEqual({
|
|
196
|
+
_tag: "success",
|
|
197
|
+
value: "2024-01-01T00:00:00Z",
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
const invalid = (await capability.handler({ ts: "not-a-date" })) as {
|
|
201
|
+
_tag: "error";
|
|
202
|
+
error: { name: string; message: string };
|
|
203
|
+
};
|
|
204
|
+
expect(invalid._tag).toBe("error");
|
|
205
|
+
expect(invalid.error.name).toBe("InvalidToolInputError");
|
|
206
|
+
});
|
|
207
|
+
|
|
164
208
|
it("accepts schema with nullable (string | null) properties", () => {
|
|
165
209
|
const capability = createToolCapability<
|
|
166
210
|
{ name: string; nickname: string | null },
|
package/src/capabilities/tool.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Ajv } from "ajv";
|
|
2
|
+
import addFormats from "ajv-formats";
|
|
2
3
|
import type { AnyJSONSchema } from "../json-schema.js";
|
|
3
4
|
import type { SchemaBuilder } from "../schema-builder.js";
|
|
4
5
|
import { getSchema } from "../schema-builder.js";
|
|
@@ -160,6 +161,7 @@ export function createToolCapability<
|
|
|
160
161
|
}
|
|
161
162
|
|
|
162
163
|
const ajv = new Ajv();
|
|
164
|
+
addFormats(ajv);
|
|
163
165
|
const validateInput = ajv.compile(inputSchema);
|
|
164
166
|
const validateOutput = outputSchema ? ajv.compile(outputSchema) : null;
|
|
165
167
|
|
|
@@ -209,7 +211,9 @@ export function createToolCapability<
|
|
|
209
211
|
error: { name: error.name, message: error.message, trace: error.stack },
|
|
210
212
|
};
|
|
211
213
|
|
|
212
|
-
process.stdout.write(
|
|
214
|
+
process.stdout.write(
|
|
215
|
+
`\n<__notion_output__>${JSON.stringify(result)}</__notion_output__>\n`,
|
|
216
|
+
);
|
|
213
217
|
|
|
214
218
|
return result;
|
|
215
219
|
}
|
|
@@ -235,7 +239,9 @@ export function createToolCapability<
|
|
|
235
239
|
},
|
|
236
240
|
};
|
|
237
241
|
|
|
238
|
-
process.stdout.write(
|
|
242
|
+
process.stdout.write(
|
|
243
|
+
`\n<__notion_output__>${JSON.stringify(result)}</__notion_output__>\n`,
|
|
244
|
+
);
|
|
239
245
|
|
|
240
246
|
return result;
|
|
241
247
|
}
|
|
@@ -245,7 +251,7 @@ export function createToolCapability<
|
|
|
245
251
|
}
|
|
246
252
|
|
|
247
253
|
process.stdout.write(
|
|
248
|
-
`\n<
|
|
254
|
+
`\n<__notion_output__>${JSON.stringify({ _tag: "success", value: result })}</__notion_output__>\n`,
|
|
249
255
|
);
|
|
250
256
|
|
|
251
257
|
return {
|
|
@@ -266,7 +272,9 @@ export function createToolCapability<
|
|
|
266
272
|
error: { name: error.name, message: error.message, trace: error.stack },
|
|
267
273
|
};
|
|
268
274
|
|
|
269
|
-
process.stdout.write(
|
|
275
|
+
process.stdout.write(
|
|
276
|
+
`\n<__notion_output__>${JSON.stringify(result)}</__notion_output__>\n`,
|
|
277
|
+
);
|
|
270
278
|
|
|
271
279
|
return result;
|
|
272
280
|
}
|
package/src/index.ts
CHANGED
|
@@ -33,4 +33,13 @@ export type {
|
|
|
33
33
|
PlaceValue,
|
|
34
34
|
Schedule,
|
|
35
35
|
} from "./types.js";
|
|
36
|
+
export type {
|
|
37
|
+
DatabaseConfig,
|
|
38
|
+
DatabaseHandle,
|
|
39
|
+
ManagedDatabaseConfig,
|
|
40
|
+
PacerConfig,
|
|
41
|
+
PacerDeclaration,
|
|
42
|
+
PacerHandle,
|
|
43
|
+
WorkerManifest,
|
|
44
|
+
} from "./worker.js";
|
|
36
45
|
export { Worker } from "./worker.js";
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { getPacerState, pacerWait, setPacerState } from "./pacer_internal.js";
|
|
4
|
+
|
|
5
|
+
describe("pacerWait", () => {
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
vi.useFakeTimers();
|
|
8
|
+
vi.setSystemTime(0);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
vi.useRealTimers();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("paces requests using server-provided config", async () => {
|
|
16
|
+
setPacerState({
|
|
17
|
+
pacers: {
|
|
18
|
+
api: {
|
|
19
|
+
lastScheduledAtMs: 0,
|
|
20
|
+
allowedRequests: 10,
|
|
21
|
+
intervalMs: 60_000,
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const first = pacerWait("api");
|
|
27
|
+
expect(getPacerState().pacers.api?.lastScheduledAtMs).toBe(6000);
|
|
28
|
+
await vi.advanceTimersByTimeAsync(6000);
|
|
29
|
+
await first;
|
|
30
|
+
|
|
31
|
+
const second = pacerWait("api");
|
|
32
|
+
expect(getPacerState().pacers.api?.lastScheduledAtMs).toBe(12_000);
|
|
33
|
+
await vi.advanceTimersByTimeAsync(6000);
|
|
34
|
+
await second;
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("does not wait when the next slot is already in the past", async () => {
|
|
38
|
+
setPacerState({
|
|
39
|
+
pacers: {
|
|
40
|
+
api: {
|
|
41
|
+
lastScheduledAtMs: 0,
|
|
42
|
+
allowedRequests: 10,
|
|
43
|
+
intervalMs: 60_000,
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const first = pacerWait("api");
|
|
49
|
+
await vi.advanceTimersByTimeAsync(6000);
|
|
50
|
+
await first;
|
|
51
|
+
|
|
52
|
+
vi.advanceTimersByTime(20_000);
|
|
53
|
+
|
|
54
|
+
const second = pacerWait("api");
|
|
55
|
+
expect(getPacerState().pacers.api?.lastScheduledAtMs).toBe(26_000);
|
|
56
|
+
await second;
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("works with apportioned (reduced) budget", async () => {
|
|
60
|
+
// Server apportioned: 5 requests per 10s (original was 10/10s, shared by 2 syncs)
|
|
61
|
+
setPacerState({
|
|
62
|
+
pacers: {
|
|
63
|
+
shared: {
|
|
64
|
+
lastScheduledAtMs: 0,
|
|
65
|
+
allowedRequests: 5,
|
|
66
|
+
intervalMs: 10_000,
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Pace should be 10_000 / 5 = 2000ms between requests
|
|
72
|
+
const first = pacerWait("shared");
|
|
73
|
+
expect(getPacerState().pacers.shared?.lastScheduledAtMs).toBe(2000);
|
|
74
|
+
await vi.advanceTimersByTimeAsync(2000);
|
|
75
|
+
await first;
|
|
76
|
+
|
|
77
|
+
const second = pacerWait("shared");
|
|
78
|
+
expect(getPacerState().pacers.shared?.lastScheduledAtMs).toBe(4000);
|
|
79
|
+
await vi.advanceTimersByTimeAsync(2000);
|
|
80
|
+
await second;
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("throws when pacer key not found in runtime context", async () => {
|
|
84
|
+
setPacerState({ pacers: {} });
|
|
85
|
+
|
|
86
|
+
await expect(pacerWait("unknown")).rejects.toThrow(
|
|
87
|
+
'Pacer "unknown" not found',
|
|
88
|
+
);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("preserves allowedRequests and intervalMs across calls", async () => {
|
|
92
|
+
setPacerState({
|
|
93
|
+
pacers: {
|
|
94
|
+
api: {
|
|
95
|
+
lastScheduledAtMs: 0,
|
|
96
|
+
allowedRequests: 10,
|
|
97
|
+
intervalMs: 60_000,
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
await vi.advanceTimersByTimeAsync(6000);
|
|
103
|
+
await pacerWait("api");
|
|
104
|
+
|
|
105
|
+
// Config should still be present after wait
|
|
106
|
+
const entry = getPacerState().pacers.api;
|
|
107
|
+
expect(entry?.allowedRequests).toBe(10);
|
|
108
|
+
expect(entry?.intervalMs).toBe(60_000);
|
|
109
|
+
});
|
|
110
|
+
});
|
package/src/pacer.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pacer module for rate limiting API requests.
|
|
3
|
+
*
|
|
4
|
+
* The preferred API is the handle returned by `worker.pacer()`:
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* const apiPacer = worker.pacer("myApi", { allowedRequests: 10, intervalMs: 1000 });
|
|
8
|
+
* await apiPacer.wait();
|
|
9
|
+
* ```
|
|
10
|
+
*
|
|
11
|
+
* @module
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { pacerWait } from "./pacer_internal.js";
|
|
15
|
+
|
|
16
|
+
export const Pacer = {
|
|
17
|
+
/**
|
|
18
|
+
* Wait until a request can proceed under the named pacer's rate limit.
|
|
19
|
+
*
|
|
20
|
+
* Prefer using the handle-based API instead: `await myPacer.wait()`.
|
|
21
|
+
*
|
|
22
|
+
* @param key - The pacer key, matching the key used in `worker.pacer()`.
|
|
23
|
+
*/
|
|
24
|
+
wait: pacerWait,
|
|
25
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal pacer state helpers used by the runtime.
|
|
3
|
+
* @internal
|
|
4
|
+
*/
|
|
5
|
+
export type PacerEntry = {
|
|
6
|
+
lastScheduledAtMs: number;
|
|
7
|
+
/** Server-provided (apportioned) request budget. */
|
|
8
|
+
allowedRequests: number;
|
|
9
|
+
/** Server-provided interval window in milliseconds. */
|
|
10
|
+
intervalMs: number;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type PacerState = {
|
|
14
|
+
pacers: Record<string, PacerEntry>;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
let pacerState: PacerState = { pacers: {} };
|
|
18
|
+
|
|
19
|
+
export function setPacerState(state: PacerState): void {
|
|
20
|
+
pacerState = state;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getPacerState(): PacerState {
|
|
24
|
+
return pacerState;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Core wait logic for pacing API requests.
|
|
29
|
+
* Sleeps if needed to stay under the rate limit.
|
|
30
|
+
* @internal
|
|
31
|
+
*/
|
|
32
|
+
export async function pacerWait(key: string): Promise<void> {
|
|
33
|
+
const state = getPacerState();
|
|
34
|
+
const entry = state.pacers[key];
|
|
35
|
+
|
|
36
|
+
if (!entry) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
`Pacer "${key}" not found. Make sure you declared it with worker.pacer("${key}", ...) ` +
|
|
39
|
+
`and are running inside the worker runtime.`,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const { allowedRequests, intervalMs } = entry;
|
|
44
|
+
const paceMs = Math.ceil(intervalMs / allowedRequests);
|
|
45
|
+
const now = Date.now();
|
|
46
|
+
const lastScheduledAtMs = entry.lastScheduledAtMs;
|
|
47
|
+
|
|
48
|
+
// Schedule at the later of: (last scheduled + pace) or now
|
|
49
|
+
const scheduledAtMs = Math.max(lastScheduledAtMs + paceMs, now);
|
|
50
|
+
const delayMs = scheduledAtMs - now;
|
|
51
|
+
|
|
52
|
+
// Update state with new scheduled timestamp
|
|
53
|
+
state.pacers[key] = { ...entry, lastScheduledAtMs: scheduledAtMs };
|
|
54
|
+
setPacerState(state);
|
|
55
|
+
|
|
56
|
+
// Sleep if needed
|
|
57
|
+
if (delayMs > 0) {
|
|
58
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
59
|
+
}
|
|
60
|
+
}
|
package/src/schema.ts
CHANGED
|
@@ -68,7 +68,7 @@ export type PropertyConfiguration =
|
|
|
68
68
|
| {
|
|
69
69
|
type: "relation";
|
|
70
70
|
/**
|
|
71
|
-
* The export name of the sync capability that defines the related
|
|
71
|
+
* The export name of the sync capability that defines the related database.
|
|
72
72
|
* This must match the export name used when defining the related sync capability.
|
|
73
73
|
*/
|
|
74
74
|
relatedSyncKey: string;
|
|
@@ -76,13 +76,6 @@ export type PropertyConfiguration =
|
|
|
76
76
|
};
|
|
77
77
|
|
|
78
78
|
export type Schema<PK extends string> = {
|
|
79
|
-
/**
|
|
80
|
-
* The default name for the database when it is first created.
|
|
81
|
-
*
|
|
82
|
-
* Updating this after the database has been created will not change the
|
|
83
|
-
* name of the database.
|
|
84
|
-
*/
|
|
85
|
-
defaultName: string;
|
|
86
79
|
/**
|
|
87
80
|
* Optional icon to use as the icon for the database page.
|
|
88
81
|
* If not provided, defaults to 📋.
|
|
@@ -211,9 +204,9 @@ export function place(): PropertyConfiguration {
|
|
|
211
204
|
|
|
212
205
|
/**
|
|
213
206
|
* Creates a relation property definition that references another sync capability.
|
|
214
|
-
* The related
|
|
207
|
+
* The related database must be defined by a sync capability in the same worker.
|
|
215
208
|
*
|
|
216
|
-
* @param relatedSyncKey - The export name of the sync capability that defines the related
|
|
209
|
+
* @param relatedSyncKey - The export name of the sync capability that defines the related database.
|
|
217
210
|
* @example
|
|
218
211
|
* ```typescript
|
|
219
212
|
* export const projectsSync = sync({...});
|
package/src/types.ts
CHANGED
|
@@ -236,7 +236,7 @@ export type RelationReference = {
|
|
|
236
236
|
|
|
237
237
|
/**
|
|
238
238
|
* Relation value representing references to related records.
|
|
239
|
-
* Each reference identifies a record in the target
|
|
239
|
+
* Each reference identifies a record in the target database.
|
|
240
240
|
*/
|
|
241
241
|
export type RelationValue = RelationReference[];
|
|
242
242
|
|
|
@@ -256,15 +256,17 @@ export type IntervalString = `${number}${TimeUnit}`;
|
|
|
256
256
|
/**
|
|
257
257
|
* Schedule configuration for sync capabilities.
|
|
258
258
|
* - "continuous": Run as frequently as the system allows
|
|
259
|
+
* - "manual": Only run when explicitly triggered
|
|
259
260
|
* - IntervalString: Run at specified intervals, e.g. "30m", "1h", "1d"
|
|
260
261
|
*/
|
|
261
|
-
export type Schedule = "continuous" | IntervalString;
|
|
262
|
+
export type Schedule = "continuous" | "manual" | IntervalString;
|
|
262
263
|
|
|
263
264
|
/**
|
|
264
265
|
* Normalized schedule representation stored in the backend.
|
|
265
266
|
*/
|
|
266
267
|
export type SyncSchedule =
|
|
267
268
|
| { type: "continuous" }
|
|
269
|
+
| { type: "manual" }
|
|
268
270
|
| { type: "interval"; intervalMs: number };
|
|
269
271
|
|
|
270
272
|
export type HandlerOptions = {
|