@notionhq/workers 0.1.0 → 0.3.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.
Files changed (67) hide show
  1. package/dist/ajv-formats.d.js +0 -0
  2. package/dist/capabilities/ai_connector.d.ts +97 -0
  3. package/dist/capabilities/ai_connector.d.ts.map +1 -0
  4. package/dist/capabilities/ai_connector.js +54 -0
  5. package/dist/capabilities/ai_connector.test.d.ts +2 -0
  6. package/dist/capabilities/ai_connector.test.d.ts.map +1 -0
  7. package/dist/capabilities/automation.d.ts.map +1 -1
  8. package/dist/capabilities/automation.js +10 -10
  9. package/dist/capabilities/output.d.ts +5 -0
  10. package/dist/capabilities/output.d.ts.map +1 -0
  11. package/dist/capabilities/output.js +10 -0
  12. package/dist/capabilities/stateful-capability.d.ts +30 -0
  13. package/dist/capabilities/stateful-capability.d.ts.map +1 -0
  14. package/dist/capabilities/stateful-capability.js +50 -0
  15. package/dist/capabilities/stateful-capability.test.d.ts +2 -0
  16. package/dist/capabilities/stateful-capability.test.d.ts.map +1 -0
  17. package/dist/capabilities/sync.d.ts +77 -30
  18. package/dist/capabilities/sync.d.ts.map +1 -1
  19. package/dist/capabilities/sync.js +26 -64
  20. package/dist/capabilities/tool.d.ts.map +1 -1
  21. package/dist/capabilities/tool.js +7 -14
  22. package/dist/capabilities/webhook.d.ts +101 -0
  23. package/dist/capabilities/webhook.d.ts.map +1 -0
  24. package/dist/capabilities/webhook.js +47 -0
  25. package/dist/index.d.ts +4 -0
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +2 -0
  28. package/dist/pacer.d.ts +24 -0
  29. package/dist/pacer.d.ts.map +1 -0
  30. package/dist/pacer.js +14 -0
  31. package/dist/pacer.test.d.ts +2 -0
  32. package/dist/pacer.test.d.ts.map +1 -0
  33. package/dist/pacer_internal.d.ts +13 -0
  34. package/dist/pacer_internal.d.ts.map +1 -0
  35. package/dist/pacer_internal.js +49 -0
  36. package/dist/schema.d.ts +28 -18
  37. package/dist/schema.d.ts.map +1 -1
  38. package/dist/schema.js +2 -2
  39. package/dist/types.d.ts +5 -2
  40. package/dist/types.d.ts.map +1 -1
  41. package/dist/worker.d.ts +222 -7
  42. package/dist/worker.d.ts.map +1 -1
  43. package/dist/worker.js +184 -8
  44. package/dist/worker.test.d.ts +2 -0
  45. package/dist/worker.test.d.ts.map +1 -0
  46. package/package.json +7 -2
  47. package/src/ajv-formats.d.ts +7 -0
  48. package/src/capabilities/ai_connector.test.ts +341 -0
  49. package/src/capabilities/ai_connector.ts +194 -0
  50. package/src/capabilities/automation.test.ts +2 -2
  51. package/src/capabilities/automation.ts +10 -6
  52. package/src/capabilities/output.ts +8 -0
  53. package/src/capabilities/stateful-capability.test.ts +25 -0
  54. package/src/capabilities/stateful-capability.ts +87 -0
  55. package/src/capabilities/sync.test.ts +178 -35
  56. package/src/capabilities/sync.ts +63 -100
  57. package/src/capabilities/tool.test.ts +47 -3
  58. package/src/capabilities/tool.ts +7 -6
  59. package/src/capabilities/webhook.ts +148 -0
  60. package/src/index.ts +31 -0
  61. package/src/pacer.test.ts +110 -0
  62. package/src/pacer.ts +25 -0
  63. package/src/pacer_internal.ts +94 -0
  64. package/src/schema.ts +29 -19
  65. package/src/types.ts +4 -2
  66. package/src/worker.test.ts +167 -0
  67. package/src/worker.ts +335 -10
@@ -0,0 +1,148 @@
1
+ import { ExecutionError } from "../error.js";
2
+ import type { HandlerOptions } from "../types.js";
3
+ import type { CapabilityContext } from "./context.js";
4
+ import { createCapabilityContext } from "./context.js";
5
+ import { writeOutput } from "./output.js";
6
+
7
+ /**
8
+ * A single incoming webhook request.
9
+ */
10
+ export interface WebhookEvent {
11
+ /**
12
+ * Unique ID for this webhook delivery, stable across retries.
13
+ * Best-effort idempotency key — prefer using the webhook provider's
14
+ * own delivery/event ID when available, since providers may redeliver
15
+ * the same event with a new deliveryId.
16
+ */
17
+ deliveryId: string;
18
+ /**
19
+ * The parsed JSON body of the incoming request, or an empty object if
20
+ * the body is not valid JSON.
21
+ */
22
+ body: Record<string, unknown>;
23
+ /**
24
+ * The raw request body as a string, useful for signature verification.
25
+ */
26
+ rawBody: string;
27
+ /**
28
+ * The HTTP headers from the incoming request
29
+ */
30
+ headers: Record<string, string>;
31
+ /**
32
+ * The HTTP method of the incoming request (e.g. "POST")
33
+ */
34
+ method: string;
35
+ }
36
+
37
+ /**
38
+ * Throw this error from your webhook execute handler to signal that
39
+ * signature verification failed. After 5 consecutive verification
40
+ * failures, the platform will short-circuit and reject all incoming
41
+ * requests for this webhook without executing the handler.
42
+ *
43
+ * @example
44
+ * ```ts
45
+ * worker.webhook("onGithubPush", {
46
+ * title: "GitHub Push",
47
+ * description: "Handles GitHub push events",
48
+ * execute: async (events) => {
49
+ * const secret = process.env.GITHUB_WEBHOOK_SECRET;
50
+ * if (!secret) {
51
+ * throw new WebhookVerificationError("GITHUB_WEBHOOK_SECRET not set");
52
+ * }
53
+ * for (const event of events) {
54
+ * if (!verifyGitHubSignature(event.rawBody, event.headers, secret)) {
55
+ * throw new WebhookVerificationError("Invalid GitHub signature");
56
+ * }
57
+ * // ... handle verified event
58
+ * }
59
+ * },
60
+ * });
61
+ * ```
62
+ */
63
+ export class WebhookVerificationError extends ExecutionError {
64
+ constructor(cause?: unknown) {
65
+ super(cause ?? "Webhook signature verification failed");
66
+ this.name = "WebhookVerificationError";
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Configuration for a webhook capability
72
+ */
73
+ export interface WebhookConfiguration {
74
+ /**
75
+ * Title of the webhook - shown in the UI when viewing webhooks
76
+ */
77
+ title: string;
78
+
79
+ /**
80
+ * Description of what this webhook does - shown in the UI
81
+ */
82
+ description: string;
83
+
84
+ /**
85
+ * The function that executes when the webhook is triggered.
86
+ * Receives an array of events (currently always a single event,
87
+ * but may be batched in the future).
88
+ *
89
+ * To signal a verification failure, throw a `WebhookVerificationError`.
90
+ * After 5 consecutive verification failures, the platform will
91
+ * short-circuit and stop executing the handler.
92
+ *
93
+ * @param events - The incoming webhook events
94
+ * @param context - The capability execution context (Notion client, etc.)
95
+ * @returns A promise that resolves when the webhook processing completes
96
+ */
97
+ execute: (
98
+ events: WebhookEvent[],
99
+ context: CapabilityContext,
100
+ ) => Promise<void> | void;
101
+ }
102
+
103
+ export type WebhookCapability = ReturnType<typeof createWebhookCapability>;
104
+
105
+ export function createWebhookCapability(
106
+ key: string,
107
+ config: WebhookConfiguration,
108
+ ) {
109
+ return {
110
+ _tag: "webhook" as const,
111
+ key,
112
+ config: {
113
+ title: config.title,
114
+ description: config.description,
115
+ },
116
+ async handler(
117
+ events: WebhookEvent[],
118
+ options?: HandlerOptions,
119
+ ): Promise<{ status: "success" } | undefined> {
120
+ try {
121
+ const capabilityContext = createCapabilityContext();
122
+ await config.execute(events, capabilityContext);
123
+
124
+ if (options?.concreteOutput) {
125
+ return { status: "success" };
126
+ } else {
127
+ writeOutput({ _tag: "success", value: { status: "success" } });
128
+ }
129
+ } catch (err) {
130
+ const error =
131
+ err instanceof ExecutionError ? err : new ExecutionError(err);
132
+
133
+ if (!options?.concreteOutput) {
134
+ writeOutput({
135
+ _tag: "error",
136
+ error: {
137
+ name: error.name,
138
+ message: error.message,
139
+ trace: error.stack,
140
+ },
141
+ });
142
+ }
143
+
144
+ throw error;
145
+ }
146
+ },
147
+ };
148
+ }
package/src/index.ts CHANGED
@@ -1,4 +1,20 @@
1
1
  export { emojiIcon, imageIcon, notionIcon, place } from "./builder.js";
2
+ export type {
3
+ AiConnectorArchetype,
4
+ AiConnectorCapability,
5
+ AiConnectorChange,
6
+ AiConnectorChangeUpsert,
7
+ AiConnectorChatAuthor,
8
+ AiConnectorChatChannel,
9
+ AiConnectorChatMessage,
10
+ AiConnectorChatRecord,
11
+ AiConnectorConfiguration,
12
+ AiConnectorExecutionResult,
13
+ AiConnectorMode,
14
+ AiConnectorProjectBlock,
15
+ AiConnectorProjectRecord,
16
+ AiConnectorRecordByArchetype,
17
+ } from "./capabilities/ai_connector.js";
2
18
  export type {
3
19
  AutomationCapability,
4
20
  AutomationConfiguration,
@@ -22,6 +38,12 @@ export type {
22
38
  SyncMode,
23
39
  } from "./capabilities/sync.js";
24
40
  export type { ToolCapability, ToolConfiguration } from "./capabilities/tool.js";
41
+ export type {
42
+ WebhookCapability,
43
+ WebhookConfiguration,
44
+ WebhookEvent,
45
+ } from "./capabilities/webhook.js";
46
+ export { WebhookVerificationError } from "./capabilities/webhook.js";
25
47
  export type { AnyJSONSchema, JSONSchema } from "./json-schema.js";
26
48
  export type { Infer, SchemaBuilder } from "./schema-builder.js";
27
49
  export { getSchema, j } from "./schema-builder.js";
@@ -33,4 +55,13 @@ export type {
33
55
  PlaceValue,
34
56
  Schedule,
35
57
  } from "./types.js";
58
+ export type {
59
+ DatabaseConfig,
60
+ DatabaseHandle,
61
+ ManagedDatabaseConfig,
62
+ PacerConfig,
63
+ PacerDeclaration,
64
+ PacerHandle,
65
+ WorkerManifest,
66
+ } from "./worker.js";
36
67
  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,94 @@
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
+ export type PacerDeclaration = {
18
+ readonly key: string;
19
+ readonly config: { allowedRequests: number; intervalMs: number };
20
+ };
21
+
22
+ let pacerState: PacerState = { pacers: {} };
23
+
24
+ export function setPacerState(state: PacerState): void {
25
+ pacerState = state;
26
+ }
27
+
28
+ export function getPacerState(): PacerState {
29
+ return pacerState;
30
+ }
31
+
32
+ /**
33
+ * Initialize pacer state from runtime context, falling back to the worker's
34
+ * own declarations for local execution.
35
+ * @internal
36
+ */
37
+ export function initPacerState(
38
+ runtimePacers: Record<string, PacerEntry> | undefined,
39
+ declarations?: readonly PacerDeclaration[],
40
+ ): void {
41
+ const pacers = runtimePacers ?? {};
42
+ if (
43
+ Object.keys(pacers).length === 0 &&
44
+ declarations &&
45
+ declarations.length > 0
46
+ ) {
47
+ const localPacers: Record<string, PacerEntry> = {};
48
+ for (const decl of declarations) {
49
+ localPacers[decl.key] = {
50
+ lastScheduledAtMs: 0,
51
+ allowedRequests: decl.config.allowedRequests,
52
+ intervalMs: decl.config.intervalMs,
53
+ };
54
+ }
55
+ setPacerState({ pacers: localPacers });
56
+ } else {
57
+ setPacerState({ pacers });
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Core wait logic for pacing API requests.
63
+ * Sleeps if needed to stay under the rate limit.
64
+ * @internal
65
+ */
66
+ export async function pacerWait(key: string): Promise<void> {
67
+ const state = getPacerState();
68
+ const entry = state.pacers[key];
69
+
70
+ if (!entry) {
71
+ throw new Error(
72
+ `Pacer "${key}" not found. Make sure you declared it with worker.pacer("${key}", ...) ` +
73
+ `and are running inside the worker runtime.`,
74
+ );
75
+ }
76
+
77
+ const { allowedRequests, intervalMs } = entry;
78
+ const paceMs = Math.ceil(intervalMs / allowedRequests);
79
+ const now = Date.now();
80
+ const lastScheduledAtMs = entry.lastScheduledAtMs;
81
+
82
+ // Schedule at the later of: (last scheduled + pace) or now
83
+ const scheduledAtMs = Math.max(lastScheduledAtMs + paceMs, now);
84
+ const delayMs = scheduledAtMs - now;
85
+
86
+ // Update state with new scheduled timestamp
87
+ state.pacers[key] = { ...entry, lastScheduledAtMs: scheduledAtMs };
88
+ setPacerState(state);
89
+
90
+ // Sleep if needed
91
+ if (delayMs > 0) {
92
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
93
+ }
94
+ }
package/src/schema.ts CHANGED
@@ -68,21 +68,14 @@ export type PropertyConfiguration =
68
68
  | {
69
69
  type: "relation";
70
70
  /**
71
- * The export name of the sync capability that defines the related collection.
72
- * This must match the export name used when defining the related sync capability.
71
+ * The key of the related database declaration.
72
+ * This must match the key passed to `worker.database()`.
73
73
  */
74
- relatedSyncKey: string;
74
+ relatedDatabaseKey: string;
75
75
  config: { twoWay: false } | { twoWay: true; relatedPropertyName: string };
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 📋.
@@ -210,32 +203,49 @@ export function place(): PropertyConfiguration {
210
203
  }
211
204
 
212
205
  /**
213
- * Creates a relation property definition that references another sync capability.
214
- * The related collection must be defined by a sync capability in the same worker.
206
+ * Creates a relation property definition that references another database.
207
+ * The related database must be declared in the same worker.
215
208
  *
216
- * @param relatedSyncKey - The export name of the sync capability that defines the related collection.
209
+ * @param relatedDatabaseKey - The key of the related database declaration.
217
210
  * @example
218
211
  * ```typescript
219
- * export const projectsSync = sync({...});
212
+ * const projects = worker.database("projects", {
213
+ * type: "managed",
214
+ * initialTitle: "Projects",
215
+ * primaryKeyProperty: "Project ID",
216
+ * schema: {
217
+ * properties: {
218
+ * "Project Name": Schema.title(),
219
+ * "Project ID": Schema.richText(),
220
+ * },
221
+ * },
222
+ * });
220
223
  *
221
- * export const tasksSync = sync({
224
+ * const tasks = worker.database("tasks", {
225
+ * type: "managed",
226
+ * initialTitle: "Tasks",
227
+ * primaryKeyProperty: "Task ID",
222
228
  * schema: {
223
229
  * properties: {
224
230
  * "Task Title": Schema.title(),
225
- * "Project": Schema.relation("projectsSync"),
231
+ * "Project": Schema.relation("projects"),
226
232
  * },
227
233
  * },
228
- * ...
234
+ * });
235
+ *
236
+ * export const tasksSync = worker.sync("tasksSync", {
237
+ * database: tasks,
238
+ * execute: async () => ({ changes: [], hasMore: false }),
229
239
  * });
230
240
  * ```
231
241
  */
232
242
  export function relation(
233
- relatedSyncKey: string,
243
+ relatedDatabaseKey: string,
234
244
  config?: { twoWay: false } | { twoWay: true; relatedPropertyName: string },
235
245
  ): PropertyConfiguration {
236
246
  return {
237
247
  type: "relation",
238
- relatedSyncKey,
248
+ relatedDatabaseKey,
239
249
  config: config ?? { twoWay: false },
240
250
  };
241
251
  }
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 collection.
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 = {
@@ -0,0 +1,167 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import * as Schema from "./schema.js";
4
+ import { Worker } from "./worker.js";
5
+
6
+ describe("Worker", () => {
7
+ it("includes sdkVersion, databases, and capabilities in manifest", () => {
8
+ const worker = new Worker();
9
+ const tasks = worker.database("tasks", {
10
+ type: "managed",
11
+ initialTitle: "Tasks",
12
+ primaryKeyProperty: "Name",
13
+ schema: {
14
+ properties: {
15
+ Name: Schema.title(),
16
+ },
17
+ },
18
+ });
19
+
20
+ worker.sync("tasksSync", {
21
+ database: tasks,
22
+ execute: async () => ({
23
+ changes: [],
24
+ hasMore: false,
25
+ }),
26
+ });
27
+
28
+ expect(worker.manifest.sdkVersion).toMatch(/^\d+\.\d+\.\d+/);
29
+ expect(worker.manifest.databases).toEqual([
30
+ {
31
+ key: "tasks",
32
+ config: {
33
+ type: "managed",
34
+ initialTitle: "Tasks",
35
+ primaryKeyProperty: "Name",
36
+ schema: {
37
+ properties: {
38
+ Name: { type: "title" },
39
+ },
40
+ },
41
+ },
42
+ },
43
+ ]);
44
+ expect(worker.manifest.pacers).toEqual([]);
45
+ expect(worker.manifest.capabilities).toEqual([
46
+ {
47
+ _tag: "sync",
48
+ key: "tasksSync",
49
+ config: {
50
+ databaseKey: "tasks",
51
+ primaryKeyProperty: "Name",
52
+ mode: undefined,
53
+ schedule: undefined,
54
+ },
55
+ },
56
+ ]);
57
+ });
58
+
59
+ it("serializes manual schedules in the manifest", () => {
60
+ const worker = new Worker();
61
+ const tasks = worker.database("tasks", {
62
+ type: "managed",
63
+ initialTitle: "Tasks",
64
+ primaryKeyProperty: "Name",
65
+ schema: {
66
+ properties: {
67
+ Name: Schema.title(),
68
+ },
69
+ },
70
+ });
71
+
72
+ worker.sync("tasksBackfill", {
73
+ database: tasks,
74
+ schedule: "manual",
75
+ execute: async () => ({
76
+ changes: [],
77
+ hasMore: false,
78
+ }),
79
+ });
80
+
81
+ expect(worker.manifest.capabilities).toEqual([
82
+ {
83
+ _tag: "sync",
84
+ key: "tasksBackfill",
85
+ config: {
86
+ databaseKey: "tasks",
87
+ primaryKeyProperty: "Name",
88
+ mode: undefined,
89
+ schedule: {
90
+ type: "manual",
91
+ },
92
+ },
93
+ },
94
+ ]);
95
+ });
96
+
97
+ it("includes pacers in manifest and links to sync capabilities", () => {
98
+ const worker = new Worker();
99
+ const tasks = worker.database("tasks", {
100
+ type: "managed",
101
+ initialTitle: "Tasks",
102
+ primaryKeyProperty: "Name",
103
+ schema: {
104
+ properties: {
105
+ Name: Schema.title(),
106
+ },
107
+ },
108
+ });
109
+
110
+ const _jiraPacer = worker.pacer("jira", {
111
+ allowedRequests: 10,
112
+ intervalMs: 1000,
113
+ });
114
+
115
+ worker.sync("jiraBackfill", {
116
+ database: tasks,
117
+ schedule: "manual",
118
+ execute: async () => ({
119
+ changes: [],
120
+ hasMore: false,
121
+ }),
122
+ });
123
+
124
+ worker.sync("jiraDelta", {
125
+ database: tasks,
126
+ schedule: "5m",
127
+ execute: async () => ({
128
+ changes: [],
129
+ hasMore: false,
130
+ }),
131
+ });
132
+
133
+ expect(worker.manifest.pacers).toEqual([
134
+ {
135
+ key: "jira",
136
+ config: {
137
+ allowedRequests: 10,
138
+ intervalMs: 1000,
139
+ },
140
+ },
141
+ ]);
142
+ });
143
+
144
+ it("rejects duplicate keys across databases, pacers, and capabilities", () => {
145
+ const worker = new Worker();
146
+ const db = worker.database("shared-key", {
147
+ type: "managed",
148
+ initialTitle: "Tasks",
149
+ primaryKeyProperty: "Name",
150
+ schema: {
151
+ properties: {
152
+ Name: Schema.title(),
153
+ },
154
+ },
155
+ });
156
+
157
+ expect(() =>
158
+ worker.sync("shared-key", {
159
+ database: db,
160
+ execute: async () => ({
161
+ changes: [],
162
+ hasMore: false,
163
+ }),
164
+ }),
165
+ ).toThrow('Worker item with key "shared-key" already registered');
166
+ });
167
+ });