@notionhq/workers 0.1.0 → 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.
Files changed (42) hide show
  1. package/dist/ajv-formats.d.js +0 -0
  2. package/dist/capabilities/automation.js +2 -2
  3. package/dist/capabilities/sync.d.ts +73 -16
  4. package/dist/capabilities/sync.d.ts.map +1 -1
  5. package/dist/capabilities/sync.js +19 -10
  6. package/dist/capabilities/tool.d.ts.map +1 -1
  7. package/dist/capabilities/tool.js +18 -10
  8. package/dist/index.d.ts +1 -0
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/pacer.d.ts +24 -0
  11. package/dist/pacer.d.ts.map +1 -0
  12. package/dist/pacer.js +14 -0
  13. package/dist/pacer.test.d.ts +2 -0
  14. package/dist/pacer.test.d.ts.map +1 -0
  15. package/dist/pacer_internal.d.ts +6 -0
  16. package/dist/pacer_internal.d.ts.map +1 -0
  17. package/dist/pacer_internal.js +32 -0
  18. package/dist/schema.d.ts +3 -10
  19. package/dist/schema.d.ts.map +1 -1
  20. package/dist/types.d.ts +5 -2
  21. package/dist/types.d.ts.map +1 -1
  22. package/dist/worker.d.ts +138 -5
  23. package/dist/worker.d.ts.map +1 -1
  24. package/dist/worker.js +73 -7
  25. package/dist/worker.test.d.ts +2 -0
  26. package/dist/worker.test.d.ts.map +1 -0
  27. package/package.json +7 -2
  28. package/src/ajv-formats.d.ts +7 -0
  29. package/src/capabilities/automation.test.ts +2 -2
  30. package/src/capabilities/automation.ts +2 -2
  31. package/src/capabilities/sync.test.ts +89 -32
  32. package/src/capabilities/sync.ts +52 -25
  33. package/src/capabilities/tool.test.ts +47 -3
  34. package/src/capabilities/tool.ts +12 -4
  35. package/src/index.ts +9 -0
  36. package/src/pacer.test.ts +110 -0
  37. package/src/pacer.ts +25 -0
  38. package/src/pacer_internal.ts +60 -0
  39. package/src/schema.ts +3 -10
  40. package/src/types.ts +4 -2
  41. package/src/worker.test.ts +167 -0
  42. package/src/worker.ts +205 -7
@@ -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
- * A configuration object that enables synchronization between a data
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 property of the data source that maps to a "primary key" in 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
- primaryKeyProperty: PK;
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 collection.
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
- primaryKeyProperty: syncConfiguration.primaryKeyProperty,
220
- schema: syncConfiguration.schema,
235
+ databaseKey: syncConfiguration.database.key,
236
+ primaryKeyProperty: syncConfiguration.database.config.primaryKeyProperty,
221
237
  mode: syncConfiguration.mode,
222
- schedule: parseSchedule(syncConfiguration.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<output>${JSON.stringify({ _tag: "error", error: { name: error.name, message: error.message } })}</output>\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: executionResult.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<output>${JSON.stringify({ _tag: "success", value: result })}</output>\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 | undefined): SyncSchedule {
304
+ function parseSchedule(schedule: Schedule): SyncSchedule {
278
305
  if (schedule === "continuous") {
279
306
  return { type: "continuous" };
280
307
  }
281
308
 
282
- if (!schedule) {
283
- return { type: "interval", intervalMs: DEFAULT_INTERVAL_MS };
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<output>{"_tag":"success","value":"Hello, Alice!"}</output>\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<output>{"_tag":"success","value":{"data":"Data for ID 42"}}</output>\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<output>{"_tag":"error","error":{"name":"InvalidToolInputError"`,
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 },
@@ -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(`\n<output>${JSON.stringify(result)}</output>\n`);
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(`\n<output>${JSON.stringify(result)}</output>\n`);
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<output>${JSON.stringify({ _tag: "success", value: result })}</output>\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(`\n<output>${JSON.stringify(result)}</output>\n`);
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 collection.
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 collection must be defined by a sync capability in the same worker.
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 collection.
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 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 = {