@notionhq/workers 0.5.0 → 0.7.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/alpha/ajv-formats.d.js +0 -0
- package/dist/alpha/builder.d.ts +149 -0
- package/dist/alpha/builder.d.ts.map +1 -0
- package/dist/alpha/builder.js +260 -0
- package/dist/alpha/builder.test.d.ts +2 -0
- package/dist/alpha/builder.test.d.ts.map +1 -0
- package/dist/alpha/cli/build.d.ts +13 -0
- package/dist/alpha/cli/build.d.ts.map +1 -0
- package/dist/alpha/cli/build.js +36 -0
- package/dist/alpha/cli/build.test.d.ts +2 -0
- package/dist/alpha/cli/build.test.d.ts.map +1 -0
- package/dist/alpha/cli/codegen.d.ts +10 -0
- package/dist/alpha/cli/codegen.d.ts.map +1 -0
- package/dist/alpha/cli/codegen.js +41 -0
- package/dist/alpha/cli/codegen.test.d.ts +2 -0
- package/dist/alpha/cli/codegen.test.d.ts.map +1 -0
- package/dist/alpha/cli/discover.d.ts +35 -0
- package/dist/alpha/cli/discover.d.ts.map +1 -0
- package/dist/alpha/cli/discover.js +39 -0
- package/dist/alpha/cli/discover.test.d.ts +2 -0
- package/dist/alpha/cli/discover.test.d.ts.map +1 -0
- package/dist/alpha/cli/emit-manifest.d.ts +33 -0
- package/dist/alpha/cli/emit-manifest.d.ts.map +1 -0
- package/dist/alpha/cli/emit-manifest.js +266 -0
- package/dist/alpha/cli/emit-manifest.test.d.ts +2 -0
- package/dist/alpha/cli/emit-manifest.test.d.ts.map +1 -0
- package/dist/alpha/cli/index.d.ts +3 -0
- package/dist/alpha/cli/index.d.ts.map +1 -0
- package/dist/alpha/cli/index.js +36 -0
- package/dist/alpha/context.d.ts +8 -0
- package/dist/alpha/context.d.ts.map +1 -0
- package/dist/alpha/context.js +33 -0
- package/dist/alpha/database.d.ts +90 -0
- package/dist/alpha/database.d.ts.map +1 -0
- package/dist/alpha/database.js +9 -0
- package/dist/alpha/error.d.ts +41 -0
- package/dist/alpha/error.d.ts.map +1 -0
- package/dist/alpha/error.js +25 -0
- package/dist/alpha/icon-names.d.ts +6 -0
- package/dist/alpha/icon-names.d.ts.map +1 -0
- package/dist/alpha/icon-names.js +0 -0
- package/dist/alpha/json-schema.d.ts +117 -0
- package/dist/alpha/json-schema.d.ts.map +1 -0
- package/dist/alpha/json-schema.js +0 -0
- package/dist/alpha/manifest.d.ts +44 -0
- package/dist/alpha/manifest.d.ts.map +1 -0
- package/dist/alpha/manifest.js +0 -0
- package/dist/alpha/output.d.ts +7 -0
- package/dist/alpha/output.d.ts.map +1 -0
- package/dist/alpha/output.js +8 -0
- package/dist/alpha/pacer.d.ts +74 -0
- package/dist/alpha/pacer.d.ts.map +1 -0
- package/dist/alpha/pacer.js +66 -0
- package/dist/alpha/pacer.test.d.ts +2 -0
- package/dist/alpha/pacer.test.d.ts.map +1 -0
- package/dist/alpha/schedule.d.ts +6 -0
- package/dist/alpha/schedule.d.ts.map +1 -0
- package/dist/alpha/schedule.js +43 -0
- package/dist/alpha/schedule.test.d.ts +2 -0
- package/dist/alpha/schedule.test.d.ts.map +1 -0
- package/dist/alpha/schema-builder.d.ts +79 -0
- package/dist/alpha/schema-builder.d.ts.map +1 -0
- package/dist/alpha/schema-builder.js +135 -0
- package/dist/alpha/schema.d.ts +185 -0
- package/dist/alpha/schema.d.ts.map +1 -0
- package/dist/alpha/schema.js +115 -0
- package/dist/alpha/sync.d.ts +233 -0
- package/dist/alpha/sync.d.ts.map +1 -0
- package/dist/alpha/sync.js +70 -0
- package/dist/alpha/sync.test.d.ts +2 -0
- package/dist/alpha/sync.test.d.ts.map +1 -0
- package/dist/alpha/tool.d.ts +132 -0
- package/dist/alpha/tool.d.ts.map +1 -0
- package/dist/alpha/tool.js +156 -0
- package/dist/alpha/tool.test.d.ts +2 -0
- package/dist/alpha/tool.test.d.ts.map +1 -0
- package/dist/alpha/triggers.d.ts +365 -0
- package/dist/alpha/triggers.d.ts.map +1 -0
- package/dist/alpha/triggers.generated.d.ts +246 -0
- package/dist/alpha/triggers.generated.d.ts.map +1 -0
- package/dist/alpha/triggers.generated.js +239 -0
- package/dist/alpha/triggers.js +0 -0
- package/dist/alpha/types.d.ts +228 -0
- package/dist/alpha/types.d.ts.map +1 -0
- package/dist/alpha/types.js +0 -0
- package/dist/alpha/workflow.d.ts +70 -0
- package/dist/alpha/workflow.d.ts.map +1 -0
- package/dist/alpha/workflow.js +39 -0
- package/dist/alpha/workflow.test.d.ts +2 -0
- package/dist/alpha/workflow.test.d.ts.map +1 -0
- package/dist/block.d.ts.map +1 -1
- package/dist/builder.d.ts.map +1 -1
- package/dist/builder.js +2 -6
- package/dist/capabilities/ai_connector.d.ts.map +1 -1
- package/dist/capabilities/automation.d.ts.map +1 -1
- package/dist/capabilities/context.d.ts.map +1 -1
- package/dist/capabilities/oauth.d.ts.map +1 -1
- package/dist/capabilities/output.d.ts.map +1 -1
- package/dist/capabilities/output.js +2 -4
- package/dist/capabilities/stateful-capability.d.ts.map +1 -1
- package/dist/capabilities/stateful-capability.js +2 -6
- package/dist/capabilities/sync.d.ts.map +1 -1
- package/dist/capabilities/sync.js +2 -8
- package/dist/capabilities/tool.d.ts.map +1 -1
- package/dist/capabilities/tool.js +6 -20
- package/dist/capabilities/webhook.d.ts.map +1 -1
- package/dist/capabilities/workflow.d.ts +2 -2
- package/dist/capabilities/workflow.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -7
- package/dist/json-schema.d.ts.map +1 -1
- package/dist/schema-builder.d.ts.map +1 -1
- package/dist/schema.d.ts.map +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +1 -4
- package/package.json +74 -14
- package/src/alpha/ajv-formats.d.ts +7 -0
- package/src/alpha/builder.test.ts +197 -0
- package/src/alpha/builder.ts +338 -0
- package/src/alpha/cli/build.test.ts +240 -0
- package/src/alpha/cli/build.ts +57 -0
- package/src/alpha/cli/codegen.test.ts +31 -0
- package/src/alpha/cli/codegen.ts +51 -0
- package/src/alpha/cli/discover.test.ts +43 -0
- package/src/alpha/cli/discover.ts +81 -0
- package/src/alpha/cli/emit-manifest.test.ts +45 -0
- package/src/alpha/cli/emit-manifest.ts +409 -0
- package/src/alpha/cli/index.ts +41 -0
- package/src/alpha/context.ts +61 -0
- package/src/alpha/database.ts +102 -0
- package/src/alpha/error.ts +49 -0
- package/src/alpha/icon-names.ts +890 -0
- package/src/alpha/json-schema.ts +180 -0
- package/src/alpha/manifest.ts +47 -0
- package/src/alpha/output.ts +8 -0
- package/src/alpha/pacer.test.ts +81 -0
- package/src/alpha/pacer.ts +181 -0
- package/src/alpha/schedule.test.ts +31 -0
- package/src/alpha/schedule.ts +53 -0
- package/src/alpha/schema-builder.ts +193 -0
- package/src/alpha/schema.ts +222 -0
- package/src/alpha/sync.test.ts +191 -0
- package/src/alpha/sync.ts +340 -0
- package/src/alpha/tool.test.ts +116 -0
- package/src/alpha/tool.ts +319 -0
- package/src/alpha/triggers.generated.ts +489 -0
- package/src/alpha/triggers.ts +500 -0
- package/src/alpha/types.ts +284 -0
- package/src/alpha/workflow.test.ts +84 -0
- package/src/alpha/workflow.ts +131 -0
- package/src/block.ts +1 -5
- package/src/builder.test.ts +28 -60
- package/src/builder.ts +3 -10
- package/src/capabilities/ai_connector.test.ts +9 -32
- package/src/capabilities/ai_connector.ts +5 -17
- package/src/capabilities/automation.test.ts +3 -12
- package/src/capabilities/automation.ts +3 -11
- package/src/capabilities/context.ts +3 -6
- package/src/capabilities/oauth.test.ts +1 -0
- package/src/capabilities/oauth.ts +1 -3
- package/src/capabilities/output.ts +1 -3
- package/src/capabilities/stateful-capability.test.ts +1 -0
- package/src/capabilities/stateful-capability.ts +2 -6
- package/src/capabilities/sync.test.ts +15 -45
- package/src/capabilities/sync.ts +10 -40
- package/src/capabilities/tool.test.ts +106 -146
- package/src/capabilities/tool.ts +17 -39
- package/src/capabilities/webhook.ts +3 -10
- package/src/capabilities/workflow.test.ts +10 -31
- package/src/capabilities/workflow.ts +5 -12
- package/src/index.ts +2 -12
- package/src/json-schema.test.ts +2 -4
- package/src/json-schema.ts +7 -6
- package/src/pacer.test.ts +1 -3
- package/src/pacer_internal.ts +1 -5
- package/src/schema-builder.test.ts +10 -35
- package/src/schema-builder.ts +5 -16
- package/src/schema.ts +2 -10
- package/src/types.ts +1 -7
- package/src/worker.ts +8 -34
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON Schema types compatible with both Anthropic and OpenAI structured
|
|
3
|
+
* output APIs.
|
|
4
|
+
*
|
|
5
|
+
* Constraints enforced by this type (intersection of both providers):
|
|
6
|
+
* - Objects must have `additionalProperties: false`
|
|
7
|
+
* - All object properties should be listed in `required`
|
|
8
|
+
* (use `anyOf` with `null` for optional fields)
|
|
9
|
+
* - No recursive schemas
|
|
10
|
+
* - No `allOf`, `not`, `if/then/else`
|
|
11
|
+
* - No string constraints (`minLength`, `maxLength`, `pattern`)
|
|
12
|
+
* - No number constraints (`minimum`, `maximum`, `multipleOf`)
|
|
13
|
+
* - Array `minItems` limited to 0 or 1; no `maxItems`
|
|
14
|
+
* - String `format` limited to: date-time, time, date, duration,
|
|
15
|
+
* email, hostname, ipv4, ipv6, uuid
|
|
16
|
+
* - `$ref` and `$defs` supported (no external refs)
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* String formats supported by both Anthropic and OpenAI structured outputs.
|
|
21
|
+
*/
|
|
22
|
+
export type JSONSchemaStringFormat =
|
|
23
|
+
| "date-time"
|
|
24
|
+
| "time"
|
|
25
|
+
| "date"
|
|
26
|
+
| "duration"
|
|
27
|
+
| "email"
|
|
28
|
+
| "hostname"
|
|
29
|
+
| "ipv4"
|
|
30
|
+
| "ipv6"
|
|
31
|
+
| "uuid";
|
|
32
|
+
|
|
33
|
+
// ---- Unsupported composition keywords ----
|
|
34
|
+
// Explicitly typed as `never` on concrete schema defs to produce clear
|
|
35
|
+
// errors when someone accidentally adds them.
|
|
36
|
+
|
|
37
|
+
interface ForbiddenCompositionKeywords {
|
|
38
|
+
anyOf?: never;
|
|
39
|
+
allOf?: never;
|
|
40
|
+
not?: never;
|
|
41
|
+
if?: never;
|
|
42
|
+
then?: never;
|
|
43
|
+
else?: never;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ---- Individual schema definitions ----
|
|
47
|
+
|
|
48
|
+
export interface JSONSchemaStringDef<
|
|
49
|
+
T extends string = string,
|
|
50
|
+
> extends ForbiddenCompositionKeywords {
|
|
51
|
+
type: "string";
|
|
52
|
+
description?: string;
|
|
53
|
+
format?: JSONSchemaStringFormat;
|
|
54
|
+
enum?: readonly T[];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface JSONSchemaNumberDef<
|
|
58
|
+
T extends number = number,
|
|
59
|
+
> extends ForbiddenCompositionKeywords {
|
|
60
|
+
type: "number" | "integer";
|
|
61
|
+
description?: string;
|
|
62
|
+
enum?: readonly T[];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface JSONSchemaBooleanDef extends ForbiddenCompositionKeywords {
|
|
66
|
+
type: "boolean";
|
|
67
|
+
description?: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface JSONSchemaNullDef extends ForbiddenCompositionKeywords {
|
|
71
|
+
type: "null";
|
|
72
|
+
description?: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface JSONSchemaArrayDef<E = unknown> extends ForbiddenCompositionKeywords {
|
|
76
|
+
type: "array";
|
|
77
|
+
description?: string;
|
|
78
|
+
items: JSONSchema<E>;
|
|
79
|
+
minItems?: 0 | 1;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface JSONSchemaObjectDef<
|
|
83
|
+
T extends Record<string, unknown> = Record<string, unknown>,
|
|
84
|
+
> extends ForbiddenCompositionKeywords {
|
|
85
|
+
type: "object";
|
|
86
|
+
description?: string;
|
|
87
|
+
properties: { [K in keyof T]-?: JSONSchema<T[K]> };
|
|
88
|
+
required: readonly (keyof T & string)[];
|
|
89
|
+
additionalProperties: false;
|
|
90
|
+
$defs?: Record<string, AnyJSONSchema>;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface JSONSchemaRefDef extends ForbiddenCompositionKeywords {
|
|
94
|
+
$ref: string;
|
|
95
|
+
description?: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Note: JSONSchemaAnyOfDef intentionally does NOT extend
|
|
99
|
+
// ForbiddenCompositionKeywords — it IS the composition keyword.
|
|
100
|
+
export interface JSONSchemaAnyOfDef<T = unknown> {
|
|
101
|
+
anyOf: readonly JSONSchema<T>[];
|
|
102
|
+
description?: string;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ---- Unconstrained schema (when T is unknown) ----
|
|
106
|
+
|
|
107
|
+
export type AnyJSONSchema =
|
|
108
|
+
| JSONSchemaStringDef
|
|
109
|
+
| JSONSchemaNumberDef
|
|
110
|
+
| JSONSchemaBooleanDef
|
|
111
|
+
| JSONSchemaNullDef
|
|
112
|
+
| JSONSchemaArrayDef
|
|
113
|
+
| JSONSchemaObjectDef
|
|
114
|
+
| JSONSchemaRefDef
|
|
115
|
+
| JSONSchemaAnyOfDef;
|
|
116
|
+
|
|
117
|
+
// ---- Internal type resolution ----
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Maps a TypeScript type T to its corresponding JSON Schema definition.
|
|
121
|
+
*
|
|
122
|
+
* Uses `[T] extends [X]` (non-distributing) so that union types like
|
|
123
|
+
* `string | number` fall through to `AnyJSONSchema` instead of distributing
|
|
124
|
+
* into `JSONSchemaStringDef | JSONSchemaNumberDef`.
|
|
125
|
+
*
|
|
126
|
+
* The `T & X` intersections (e.g. `T & string`) satisfy the generic
|
|
127
|
+
* constraints on each def without widening T.
|
|
128
|
+
*
|
|
129
|
+
* A `$ref` is always valid in any schema position, so `JSONSchemaRefDef`
|
|
130
|
+
* is unioned in at the end.
|
|
131
|
+
*/
|
|
132
|
+
type JSONSchemaFor<T> =
|
|
133
|
+
| JSONSchemaRefDef
|
|
134
|
+
| ([T] extends [string]
|
|
135
|
+
? JSONSchemaStringDef<T & string>
|
|
136
|
+
: [T] extends [number]
|
|
137
|
+
? JSONSchemaNumberDef<T & number>
|
|
138
|
+
: [T] extends [boolean]
|
|
139
|
+
? JSONSchemaBooleanDef
|
|
140
|
+
: [T] extends [readonly (infer E)[]]
|
|
141
|
+
? JSONSchemaArrayDef<E>
|
|
142
|
+
: [T] extends [Record<string, unknown>]
|
|
143
|
+
? JSONSchemaObjectDef<T & Record<string, unknown>>
|
|
144
|
+
: AnyJSONSchema);
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Wraps a non-null schema in an `anyOf` with `{ type: "null" }` for
|
|
148
|
+
* nullable types (e.g. `string | null`).
|
|
149
|
+
*/
|
|
150
|
+
type JSONSchemaNullable<T> = {
|
|
151
|
+
anyOf: readonly [JSONSchemaFor<T>, JSONSchemaNullDef];
|
|
152
|
+
description?: string;
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
// ---- Main exported type ----
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* A JSON Schema definition compatible with both Anthropic and OpenAI
|
|
159
|
+
* structured output APIs.
|
|
160
|
+
*
|
|
161
|
+
* When the generic parameter `T` is provided, the schema structure is
|
|
162
|
+
* constrained at compile time to match `T`:
|
|
163
|
+
*
|
|
164
|
+
* ```ts
|
|
165
|
+
* // Schema is checked against the type:
|
|
166
|
+
* const schema: JSONSchema<{ name: string }> = {
|
|
167
|
+
* type: "object",
|
|
168
|
+
* properties: { name: { type: "string" } },
|
|
169
|
+
* required: ["name"],
|
|
170
|
+
* additionalProperties: false,
|
|
171
|
+
* };
|
|
172
|
+
* ```
|
|
173
|
+
*
|
|
174
|
+
* @template T - The TypeScript type the schema should describe.
|
|
175
|
+
*/
|
|
176
|
+
export type JSONSchema<T = unknown> = unknown extends T
|
|
177
|
+
? AnyJSONSchema
|
|
178
|
+
: null extends T
|
|
179
|
+
? JSONSchemaNullable<NonNullable<T>>
|
|
180
|
+
: JSONSchemaFor<T>;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A capability type identifier (e.g. "sync", "tool"). The set of types the
|
|
3
|
+
* build tool can discover is defined by `CAPABILITY_TYPES` in the CLI; the
|
|
4
|
+
* manifest treats the type as an opaque string.
|
|
5
|
+
*/
|
|
6
|
+
export type CapabilityType = string;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* One capability in the manifest: its type, its key (derived from its
|
|
10
|
+
* filename), and its handler-free configuration.
|
|
11
|
+
*/
|
|
12
|
+
export type CapabilityManifestEntry = {
|
|
13
|
+
type: CapabilityType;
|
|
14
|
+
key: string;
|
|
15
|
+
config: unknown;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* One database in the manifest: its key (derived from its filename under
|
|
20
|
+
* `src/databases/`, or from the owning sync's key for inline databases)
|
|
21
|
+
* and its configuration, with `relation` properties resolved to
|
|
22
|
+
* `relatedDatabaseKey` references.
|
|
23
|
+
*/
|
|
24
|
+
export type DatabaseManifestEntry = {
|
|
25
|
+
key: string;
|
|
26
|
+
config: unknown;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* One pacer in the manifest: its stable name and its rate-limit config.
|
|
31
|
+
*/
|
|
32
|
+
export type PacerManifestEntry = {
|
|
33
|
+
key: string;
|
|
34
|
+
config: unknown;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The full worker manifest, emitted by `notion-workers build` as
|
|
39
|
+
* `dist/manifest.json` and read by the platform at deploy time. This is a
|
|
40
|
+
* complete, static description of the worker.
|
|
41
|
+
*/
|
|
42
|
+
export type WorkerManifest = {
|
|
43
|
+
sdkVersion: string;
|
|
44
|
+
databases: DatabaseManifestEntry[];
|
|
45
|
+
pacers: PacerManifestEntry[];
|
|
46
|
+
capabilities: CapabilityManifestEntry[];
|
|
47
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Writes a notion_output envelope to stdout using the shared wire format.
|
|
3
|
+
* The platform runner scans a run's log stream for this envelope to
|
|
4
|
+
* recover the capability's result.
|
|
5
|
+
*/
|
|
6
|
+
export function writeOutput(value: unknown): void {
|
|
7
|
+
process.stdout.write(`\n<__notion_output__>${JSON.stringify(value)}</__notion_output__>\n`);
|
|
8
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { createPacer, initPacerState, pacerStates } from "./pacer.js";
|
|
4
|
+
|
|
5
|
+
let pacerCount = 0;
|
|
6
|
+
|
|
7
|
+
/** Each test gets a uniquely named pacer; the creation registry is global. */
|
|
8
|
+
function testPacer(config = { allowedRequests: 2, intervalMs: 1000 }) {
|
|
9
|
+
return createPacer(`test-pacer-${pacerCount++}`, config);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
vi.useFakeTimers();
|
|
14
|
+
initPacerState(undefined);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
vi.useRealTimers();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe("pacer wait", () => {
|
|
22
|
+
it("spaces requests evenly over the interval window", async () => {
|
|
23
|
+
const pacer = testPacer({ allowedRequests: 2, intervalMs: 1000 });
|
|
24
|
+
|
|
25
|
+
// First wait: nothing scheduled yet, proceeds immediately.
|
|
26
|
+
await pacer.wait();
|
|
27
|
+
|
|
28
|
+
// Second wait: must be paced 500ms (1000ms / 2 requests) after the first.
|
|
29
|
+
let resolved = false;
|
|
30
|
+
const second = pacer.wait().then(() => {
|
|
31
|
+
resolved = true;
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
await vi.advanceTimersByTimeAsync(499);
|
|
35
|
+
expect(resolved).toBe(false);
|
|
36
|
+
await vi.advanceTimersByTimeAsync(1);
|
|
37
|
+
await second;
|
|
38
|
+
expect(resolved).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("paces against server-provided (apportioned) state over declared config", async () => {
|
|
42
|
+
const pacer = testPacer({ allowedRequests: 10, intervalMs: 1000 });
|
|
43
|
+
|
|
44
|
+
// The server apportioned this pacer down to 1 request per second and
|
|
45
|
+
// says a request was just scheduled.
|
|
46
|
+
initPacerState({
|
|
47
|
+
[pacer.name]: {
|
|
48
|
+
lastScheduledAtMs: Date.now(),
|
|
49
|
+
allowedRequests: 1,
|
|
50
|
+
intervalMs: 1000,
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
let resolved = false;
|
|
55
|
+
const wait = pacer.wait().then(() => {
|
|
56
|
+
resolved = true;
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Declared config would allow a request after 100ms; apportioned
|
|
60
|
+
// state requires the full 1000ms.
|
|
61
|
+
await vi.advanceTimersByTimeAsync(999);
|
|
62
|
+
expect(resolved).toBe(false);
|
|
63
|
+
await vi.advanceTimersByTimeAsync(1);
|
|
64
|
+
await wait;
|
|
65
|
+
expect(resolved).toBe(true);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("reports updated state for the platform to persist", async () => {
|
|
69
|
+
const pacer = testPacer({ allowedRequests: 2, intervalMs: 1000 });
|
|
70
|
+
|
|
71
|
+
await pacer.wait();
|
|
72
|
+
|
|
73
|
+
expect(pacerStates()).toEqual({
|
|
74
|
+
[pacer.name]: {
|
|
75
|
+
lastScheduledAtMs: Date.now(),
|
|
76
|
+
allowedRequests: 2,
|
|
77
|
+
intervalMs: 1000,
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration for a pacer that rate-limits requests to an external API.
|
|
3
|
+
* When multiple syncs share a pacer, the platform apportions the budget
|
|
4
|
+
* across them.
|
|
5
|
+
*/
|
|
6
|
+
export type PacerConfig = {
|
|
7
|
+
/** Maximum number of requests allowed per interval. */
|
|
8
|
+
allowedRequests: number;
|
|
9
|
+
/** The interval window in milliseconds. */
|
|
10
|
+
intervalMs: number;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Runtime pacing state for one pacer, as exchanged with the platform: the
|
|
15
|
+
* server sends entries with the *apportioned* budget (the declared budget
|
|
16
|
+
* divided across the syncs sharing the pacer), and the SDK returns the
|
|
17
|
+
* updated entries after a run so the server can persist `lastScheduledAtMs`.
|
|
18
|
+
* The wire shape matches v1.
|
|
19
|
+
*/
|
|
20
|
+
export type PacerEntry = {
|
|
21
|
+
lastScheduledAtMs: number;
|
|
22
|
+
/** Server-provided (apportioned) request budget. */
|
|
23
|
+
allowedRequests: number;
|
|
24
|
+
/** Server-provided interval window in milliseconds. */
|
|
25
|
+
intervalMs: number;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// Pacers register themselves at creation, keyed on globalThis so the
|
|
29
|
+
// registry is shared even if multiple copies of this module are loaded
|
|
30
|
+
// (e.g. the CLI's instance vs. the one the worker bundle resolves).
|
|
31
|
+
const REGISTRY_KEY = Symbol.for("@notionhq/workers/alpha/pacer-registry");
|
|
32
|
+
|
|
33
|
+
// Per-run pacing state, keyed on globalThis for the same reason.
|
|
34
|
+
const STATE_KEY = Symbol.for("@notionhq/workers/alpha/pacer-state");
|
|
35
|
+
|
|
36
|
+
function registry(): Map<string, Pacer> {
|
|
37
|
+
const holder = globalThis as { [REGISTRY_KEY]?: Map<string, Pacer> };
|
|
38
|
+
holder[REGISTRY_KEY] ??= new Map();
|
|
39
|
+
return holder[REGISTRY_KEY];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function state(): Map<string, PacerEntry> {
|
|
43
|
+
const holder = globalThis as { [STATE_KEY]?: Map<string, PacerEntry> };
|
|
44
|
+
holder[STATE_KEY] ??= new Map();
|
|
45
|
+
return holder[STATE_KEY];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* A pacer as returned by {@link createPacer}.
|
|
50
|
+
*/
|
|
51
|
+
export type Pacer = {
|
|
52
|
+
_tag: "pacer";
|
|
53
|
+
name: string;
|
|
54
|
+
config: PacerConfig;
|
|
55
|
+
/**
|
|
56
|
+
* Wait until a request can proceed under this pacer's rate limit.
|
|
57
|
+
* Call this before every API request the pacer covers.
|
|
58
|
+
*
|
|
59
|
+
* Requests are evenly spaced over the interval window. Inside a sync
|
|
60
|
+
* run, pacing continues from the server-provided state (which carries
|
|
61
|
+
* the budget apportioned across the syncs sharing this pacer); outside
|
|
62
|
+
* one, pacing starts fresh from the declared config.
|
|
63
|
+
*/
|
|
64
|
+
wait(): Promise<void>;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Declare a pacer for rate-limiting requests to an external API.
|
|
69
|
+
*
|
|
70
|
+
* Like databases, a pacer is a named resource that can live anywhere —
|
|
71
|
+
* typically in a shared module next to the API client it paces. Unlike
|
|
72
|
+
* databases, a pacer is not attached to sync config: handlers use as many
|
|
73
|
+
* pacers as they need by importing them and calling `wait()`. Creating a
|
|
74
|
+
* pacer registers it, so every pacer in a module the worker imports is
|
|
75
|
+
* included in the manifest automatically.
|
|
76
|
+
*
|
|
77
|
+
* The `name` is the pacer's stable identity across deploys — the platform
|
|
78
|
+
* uses it to persist pacing state. Two `createPacer(...)` calls sharing a
|
|
79
|
+
* name fail the build; share one instance via import instead.
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* ```ts
|
|
83
|
+
* // src/lib/jira.ts
|
|
84
|
+
* import { createPacer } from "@notionhq/workers/alpha/pacer";
|
|
85
|
+
*
|
|
86
|
+
* export const jiraPacer = createPacer("jira", {
|
|
87
|
+
* allowedRequests: 10,
|
|
88
|
+
* intervalMs: 1000,
|
|
89
|
+
* });
|
|
90
|
+
*
|
|
91
|
+
* // src/syncs/issues.ts — inside the handler:
|
|
92
|
+
* // await jiraPacer.wait();
|
|
93
|
+
* // const issues = await fetchIssues();
|
|
94
|
+
* ```
|
|
95
|
+
*/
|
|
96
|
+
export function createPacer(name: string, config: PacerConfig): Pacer {
|
|
97
|
+
if (!name || typeof name !== "string") {
|
|
98
|
+
throw new Error("Pacer name must be a non-empty string");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (registry().has(name)) {
|
|
102
|
+
throw new Error(
|
|
103
|
+
`Two distinct pacers are named "${name}". A name identifies one pacer — ` +
|
|
104
|
+
`share a single createPacer(...) instance via import instead of declaring it twice`,
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const pacer: Pacer = {
|
|
109
|
+
_tag: "pacer",
|
|
110
|
+
name,
|
|
111
|
+
config,
|
|
112
|
+
async wait(): Promise<void> {
|
|
113
|
+
// Fall back to the declared (unapportioned) config when the
|
|
114
|
+
// platform hasn't provided state — local runs, tools.
|
|
115
|
+
const entry = state().get(name) ?? {
|
|
116
|
+
lastScheduledAtMs: 0,
|
|
117
|
+
allowedRequests: config.allowedRequests,
|
|
118
|
+
intervalMs: config.intervalMs,
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const paceMs = Math.ceil(entry.intervalMs / entry.allowedRequests);
|
|
122
|
+
const now = Date.now();
|
|
123
|
+
|
|
124
|
+
// Schedule at the later of: (last scheduled + pace) or now.
|
|
125
|
+
const scheduledAtMs = Math.max(entry.lastScheduledAtMs + paceMs, now);
|
|
126
|
+
const delayMs = scheduledAtMs - now;
|
|
127
|
+
|
|
128
|
+
state().set(name, { ...entry, lastScheduledAtMs: scheduledAtMs });
|
|
129
|
+
|
|
130
|
+
if (delayMs > 0) {
|
|
131
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
registry().set(name, pacer);
|
|
137
|
+
return pacer;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Initialize per-run pacing state from the platform-provided entries.
|
|
142
|
+
* Entries carry the apportioned budget; pacers absent from the map pace
|
|
143
|
+
* against their declared config (see `wait()`).
|
|
144
|
+
*
|
|
145
|
+
* @internal Called by capability handlers at the start of a run.
|
|
146
|
+
*/
|
|
147
|
+
export function initPacerState(runtimePacers: Record<string, PacerEntry> | undefined): void {
|
|
148
|
+
state().clear();
|
|
149
|
+
for (const [name, entry] of Object.entries(runtimePacers ?? {})) {
|
|
150
|
+
state().set(name, entry);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* The pacing state accumulated during this run, to report back to the
|
|
156
|
+
* platform as `nextPacerStates`.
|
|
157
|
+
*
|
|
158
|
+
* @internal Called by capability handlers at the end of a run.
|
|
159
|
+
*/
|
|
160
|
+
export function pacerStates(): Record<string, PacerEntry> {
|
|
161
|
+
return Object.fromEntries(state());
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* The pacers registered so far, in creation order.
|
|
166
|
+
*
|
|
167
|
+
* @internal Used by the build to collect pacers after importing the bundle.
|
|
168
|
+
*/
|
|
169
|
+
export function registeredPacers(): Pacer[] {
|
|
170
|
+
return Array.from(registry().values());
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Clear the pacer registry.
|
|
175
|
+
*
|
|
176
|
+
* @internal Used by the build before importing a bundle, so repeated builds
|
|
177
|
+
* in one process (tests, watch mode) don't see stale registrations.
|
|
178
|
+
*/
|
|
179
|
+
export function clearRegisteredPacers(): void {
|
|
180
|
+
registry().clear();
|
|
181
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { parseSchedule } from "./schedule.js";
|
|
4
|
+
|
|
5
|
+
describe("parseSchedule", () => {
|
|
6
|
+
it("parses continuous", () => {
|
|
7
|
+
expect(parseSchedule("continuous")).toEqual({ type: "continuous" });
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("parses manual", () => {
|
|
11
|
+
expect(parseSchedule("manual")).toEqual({ type: "manual" });
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("parses interval strings", () => {
|
|
15
|
+
expect(parseSchedule("30m")).toEqual({ type: "interval", intervalMs: 30 * 60 * 1000 });
|
|
16
|
+
expect(parseSchedule("2h")).toEqual({ type: "interval", intervalMs: 2 * 60 * 60 * 1000 });
|
|
17
|
+
expect(parseSchedule("1d")).toEqual({ type: "interval", intervalMs: 24 * 60 * 60 * 1000 });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("rejects malformed schedules", () => {
|
|
21
|
+
// @ts-expect-error deliberately malformed
|
|
22
|
+
expect(() => parseSchedule("5s")).toThrow(/Invalid schedule format/);
|
|
23
|
+
// @ts-expect-error deliberately malformed
|
|
24
|
+
expect(() => parseSchedule("daily")).toThrow(/Invalid schedule format/);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("rejects out-of-range intervals", () => {
|
|
28
|
+
expect(() => parseSchedule("0m")).toThrow(/at least 1 minute/);
|
|
29
|
+
expect(() => parseSchedule("8d")).toThrow(/at most 7 days/);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { NormalizedSchedule, Schedule, TimeUnit } from "./types.js";
|
|
2
|
+
|
|
3
|
+
const MS_PER_MINUTE = 60 * 1000;
|
|
4
|
+
const MS_PER_HOUR = 60 * MS_PER_MINUTE;
|
|
5
|
+
const MS_PER_DAY = 24 * MS_PER_HOUR;
|
|
6
|
+
|
|
7
|
+
const MIN_INTERVAL_MS = MS_PER_MINUTE; // 1m
|
|
8
|
+
const MAX_INTERVAL_MS = 7 * MS_PER_DAY; // 7d
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Parses a user-friendly schedule string into the normalized backend format.
|
|
12
|
+
*/
|
|
13
|
+
export function parseSchedule(schedule: Schedule): NormalizedSchedule {
|
|
14
|
+
if (schedule === "continuous") {
|
|
15
|
+
return { type: "continuous" };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (schedule === "manual") {
|
|
19
|
+
return { type: "manual" };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const match = schedule.match(/^(\d+)(m|h|d)$/);
|
|
23
|
+
if (!match?.[1] || !match[2]) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
`Invalid schedule format: "${schedule}". Use "continuous" or an interval like "30m", "1h", "1d".`,
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const value = parseInt(match[1], 10);
|
|
30
|
+
const unit = match[2] as TimeUnit;
|
|
31
|
+
|
|
32
|
+
let intervalMs: number;
|
|
33
|
+
switch (unit) {
|
|
34
|
+
case "m":
|
|
35
|
+
intervalMs = value * MS_PER_MINUTE;
|
|
36
|
+
break;
|
|
37
|
+
case "h":
|
|
38
|
+
intervalMs = value * MS_PER_HOUR;
|
|
39
|
+
break;
|
|
40
|
+
case "d":
|
|
41
|
+
intervalMs = value * MS_PER_DAY;
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (intervalMs < MIN_INTERVAL_MS) {
|
|
46
|
+
throw new Error(`Schedule interval must be at least 1 minute. Got: "${schedule}"`);
|
|
47
|
+
}
|
|
48
|
+
if (intervalMs > MAX_INTERVAL_MS) {
|
|
49
|
+
throw new Error(`Schedule interval must be at most 7 days. Got: "${schedule}"`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return { type: "interval", intervalMs };
|
|
53
|
+
}
|