notion-mcp-server 1.0.1 → 2.4.2
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/README.md +383 -192
- package/build/config/index.js +3 -1
- package/build/dispatch/concurrency.js +15 -0
- package/build/dispatch/idempotency.js +38 -0
- package/build/dispatch/index.js +175 -0
- package/build/dispatch/rate-limit.js +56 -0
- package/build/dispatch/retry.js +97 -0
- package/build/index.js +1 -1
- package/build/markdown/parse.js +265 -0
- package/build/operations/blocks.js +331 -0
- package/build/operations/comments.js +191 -0
- package/build/operations/data-sources.js +85 -0
- package/build/operations/databases.js +345 -0
- package/build/operations/files.js +239 -0
- package/build/operations/index.js +19 -0
- package/build/operations/pages.js +486 -0
- package/build/operations/registry.js +16 -0
- package/build/operations/users.js +101 -0
- package/build/prompts/index.js +105 -0
- package/build/schema/blocks.js +19 -138
- package/build/schema/database.js +27 -111
- package/build/schema/emit.js +68 -0
- package/build/schema/file.js +1 -1
- package/build/schema/filter-dsl.js +333 -0
- package/build/schema/icon.js +1 -1
- package/build/schema/page-properties.js +17 -3
- package/build/schema/page.js +12 -125
- package/build/schema/refs.js +16 -0
- package/build/schema/rich-text.js +1 -1
- package/build/server/index.js +16 -3
- package/build/services/auth.js +19 -0
- package/build/services/notion.js +14 -17
- package/build/tools/index.js +119 -21
- package/build/utils/error.js +125 -86
- package/build/utils/handler.js +11 -0
- package/build/utils/learning-error.js +40 -0
- package/build/utils/notion-types.js +16 -0
- package/build/utils/paginate.js +35 -0
- package/build/utils/schema-slice.js +156 -0
- package/build/utils/slim.js +269 -0
- package/package.json +13 -7
- package/build/resources/imageList.js +0 -62
- package/build/resources/index.js +0 -1
- package/build/resources/predictionList.js +0 -43
- package/build/resources/svgList.js +0 -69
- package/build/schema/comments.js +0 -60
- package/build/schema/notion.js +0 -57
- package/build/schema/richText.js +0 -757
- package/build/schema/tools.js +0 -17
- package/build/schema/users.js +0 -39
- package/build/services/loggs.js +0 -13
- package/build/services/replicate.js +0 -23
- package/build/tools/appendBlockChildren.js +0 -25
- package/build/tools/batchAppendBlockChildren.js +0 -33
- package/build/tools/batchDeleteBlocks.js +0 -32
- package/build/tools/batchMixedOperations.js +0 -58
- package/build/tools/batchUpdateBlocks.js +0 -33
- package/build/tools/blocks.js +0 -34
- package/build/tools/comments.js +0 -81
- package/build/tools/createDatabase.js +0 -18
- package/build/tools/createPage.js +0 -18
- package/build/tools/createPrediction.js +0 -28
- package/build/tools/database.js +0 -16
- package/build/tools/deleteBlock.js +0 -24
- package/build/tools/formatRichText.js +0 -83
- package/build/tools/generateImage.js +0 -48
- package/build/tools/generateImageVariants.js +0 -105
- package/build/tools/generateMultipleImages.js +0 -60
- package/build/tools/generateSVG.js +0 -43
- package/build/tools/getPrediction.js +0 -22
- package/build/tools/pages.js +0 -22
- package/build/tools/predictionList.js +0 -30
- package/build/tools/queryDatabase.js +0 -22
- package/build/tools/retrieveBlock.js +0 -24
- package/build/tools/retrieveBlockChildren.js +0 -32
- package/build/tools/searchPage.js +0 -24
- package/build/tools/updateBlock.js +0 -25
- package/build/tools/updateDatabase.js +0 -18
- package/build/tools/updatePage.js +0 -40
- package/build/tools/updatePageProperties.js +0 -21
- package/build/tools/users.js +0 -75
- package/build/types/blocks.js +0 -12
- package/build/types/comments.js +0 -7
- package/build/types/database.js +0 -6
- package/build/types/notion.js +0 -1
- package/build/types/page.js +0 -8
- package/build/types/richText.js +0 -1
- package/build/types/tools.js +0 -1
- package/build/types/users.js +0 -6
- package/build/utils/blob.js +0 -5
- package/build/utils/image.js +0 -34
- package/build/utils/index.js +0 -1
- package/build/utils/richText.js +0 -174
- package/build/validation/blocks.js +0 -568
- package/build/validation/notion.js +0 -51
- package/build/validation/page.js +0 -262
- package/build/validation/richText.js +0 -744
- package/build/validation/tools.js +0 -16
- /package/build/{types/index.js → operations/types.js} +0 -0
package/build/config/index.js
CHANGED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export async function mapWithConcurrency(items, limit, fn) {
|
|
2
|
+
const results = new Array(items.length);
|
|
3
|
+
let cursor = 0;
|
|
4
|
+
async function worker() {
|
|
5
|
+
while (true) {
|
|
6
|
+
const index = cursor++;
|
|
7
|
+
if (index >= items.length)
|
|
8
|
+
return;
|
|
9
|
+
results[index] = await fn(items[index], index);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
const workerCount = Math.max(1, Math.min(limit, items.length));
|
|
13
|
+
await Promise.all(Array.from({ length: workerCount }, () => worker()));
|
|
14
|
+
return results;
|
|
15
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const TTL_MS = 5 * 60 * 1000;
|
|
2
|
+
const MAX_ENTRIES = 512;
|
|
3
|
+
const cache = new Map();
|
|
4
|
+
function evictExpired(now) {
|
|
5
|
+
for (const [key, entry] of cache) {
|
|
6
|
+
if (entry.expiresAt <= now)
|
|
7
|
+
cache.delete(key);
|
|
8
|
+
}
|
|
9
|
+
if (cache.size > MAX_ENTRIES) {
|
|
10
|
+
const overflow = cache.size - MAX_ENTRIES;
|
|
11
|
+
let removed = 0;
|
|
12
|
+
for (const key of cache.keys()) {
|
|
13
|
+
if (removed >= overflow)
|
|
14
|
+
break;
|
|
15
|
+
cache.delete(key);
|
|
16
|
+
removed++;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export function lookup(key) {
|
|
21
|
+
const now = Date.now();
|
|
22
|
+
const entry = cache.get(key);
|
|
23
|
+
if (!entry)
|
|
24
|
+
return undefined;
|
|
25
|
+
if (entry.expiresAt <= now) {
|
|
26
|
+
cache.delete(key);
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
return entry.result;
|
|
30
|
+
}
|
|
31
|
+
export function store(key, result) {
|
|
32
|
+
const now = Date.now();
|
|
33
|
+
evictExpired(now);
|
|
34
|
+
cache.set(key, { result, expiresAt: now + TTL_MS });
|
|
35
|
+
}
|
|
36
|
+
export function buildKey(operation, idempotencyKey) {
|
|
37
|
+
return `${operation}::${idempotencyKey}`;
|
|
38
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { getOperation, operationNames } from "../operations/registry.js";
|
|
3
|
+
import { buildKey, lookup, store } from "./idempotency.js";
|
|
4
|
+
import { mapWithConcurrency } from "./concurrency.js";
|
|
5
|
+
import { rateLimiter } from "./rate-limit.js";
|
|
6
|
+
import { isRetryableErrorCode, withRetry } from "./retry.js";
|
|
7
|
+
import { buildValidationError } from "../utils/learning-error.js";
|
|
8
|
+
import { toErrorEnvelope } from "../utils/error.js";
|
|
9
|
+
const DEFAULT_CONCURRENCY = 3;
|
|
10
|
+
const MAX_CONCURRENCY = 10;
|
|
11
|
+
function isBatchPayload(payload) {
|
|
12
|
+
return (typeof payload === "object" &&
|
|
13
|
+
payload !== null &&
|
|
14
|
+
Array.isArray(payload.items));
|
|
15
|
+
}
|
|
16
|
+
function unknownOperationError(name) {
|
|
17
|
+
return {
|
|
18
|
+
code: "unknown_operation",
|
|
19
|
+
message: `Unknown operation: "${name}". Use notion_describe with a valid operation name, or check the notion://operations resource for the full list.`,
|
|
20
|
+
fix: `Available operations: ${operationNames().join(", ")}`,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export async function dispatch(operationName, payload) {
|
|
24
|
+
const def = getOperation(operationName);
|
|
25
|
+
if (!def) {
|
|
26
|
+
return { ok: false, error: unknownOperationError(operationName) };
|
|
27
|
+
}
|
|
28
|
+
if (isBatchPayload(payload)) {
|
|
29
|
+
if (!def.batchable) {
|
|
30
|
+
// batch_mixed_blocks looks batch-shaped but uses its own `operations[]`
|
|
31
|
+
// envelope (mixed op kinds, no per-item rollback). Point callers at the
|
|
32
|
+
// right shape instead of the generic not_batchable message.
|
|
33
|
+
if (operationName === "batch_mixed_blocks") {
|
|
34
|
+
return {
|
|
35
|
+
ok: false,
|
|
36
|
+
error: {
|
|
37
|
+
code: "wrong_envelope",
|
|
38
|
+
message: 'batch_mixed_blocks uses its own envelope: { operations: [{ op: "append"|"update"|"delete", ... }] }. The universal { items: [...] } envelope does not apply here.',
|
|
39
|
+
fix: 'Wrap your operations as { operations: [{ op: "append", block_id, markdown }, { op: "update", ... }, { op: "delete", ... }] }. Or use the items[] form on append_blocks / update_block / delete_block for single-kind batches.',
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
ok: false,
|
|
45
|
+
error: {
|
|
46
|
+
code: "not_batchable",
|
|
47
|
+
message: `Operation "${operationName}" does not support batch mode.`,
|
|
48
|
+
fix: "Call it with a single payload object instead of { items: [...] }.",
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
return runBatch(def, payload);
|
|
53
|
+
}
|
|
54
|
+
return runSingle(def, payload);
|
|
55
|
+
}
|
|
56
|
+
// Run the handler under the shared rate limiter, retrying on transient SDK
|
|
57
|
+
// failures. Token is acquired inside withRetry so each retry attempt counts
|
|
58
|
+
// against the per-second budget instead of bursting on retry storms.
|
|
59
|
+
function runHandlerWithLimitAndRetry(def, params) {
|
|
60
|
+
return withRetry(async () => {
|
|
61
|
+
await rateLimiter.acquire();
|
|
62
|
+
return def.handler(params);
|
|
63
|
+
}, { isRetryableResult: (r) => r.ok === false && isRetryableErrorCode(r.error.code) });
|
|
64
|
+
}
|
|
65
|
+
async function runSingle(def, payload) {
|
|
66
|
+
const parsed = def.schema.safeParse(payload);
|
|
67
|
+
if (!parsed.success) {
|
|
68
|
+
return { ok: false, error: buildValidationError(def, parsed.error) };
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
return await runHandlerWithLimitAndRetry(def, parsed.data);
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
return { ok: false, error: toErrorEnvelope(error) };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async function runBatch(def, payload) {
|
|
78
|
+
const idempotencyKey = payload.idempotency_key;
|
|
79
|
+
if (idempotencyKey) {
|
|
80
|
+
const cached = lookup(buildKey(def.name, idempotencyKey));
|
|
81
|
+
if (cached)
|
|
82
|
+
return cached;
|
|
83
|
+
}
|
|
84
|
+
const atomic = payload.atomic === true;
|
|
85
|
+
// Atomic mode requires serial execution: with concurrency > 1, the `aborted`
|
|
86
|
+
// flag is set only after the first failure resolves, but other workers have
|
|
87
|
+
// already started in-flight requests, so later items execute when they
|
|
88
|
+
// shouldn't. Force concurrency=1 to make the abort barrier reliable.
|
|
89
|
+
const requested = payload.concurrency ?? DEFAULT_CONCURRENCY;
|
|
90
|
+
const concurrency = atomic ? 1 : Math.max(1, Math.min(requested, MAX_CONCURRENCY));
|
|
91
|
+
const items = payload.items;
|
|
92
|
+
const createdForRollback = [];
|
|
93
|
+
let aborted = false;
|
|
94
|
+
const results = await mapWithConcurrency(items, concurrency, async (item, index) => {
|
|
95
|
+
if (aborted) {
|
|
96
|
+
return {
|
|
97
|
+
index,
|
|
98
|
+
ok: false,
|
|
99
|
+
error: {
|
|
100
|
+
code: "aborted",
|
|
101
|
+
message: "Skipped: a prior item failed in atomic batch.",
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
const parsed = def.schema.safeParse(item);
|
|
106
|
+
if (!parsed.success) {
|
|
107
|
+
const failure = {
|
|
108
|
+
index,
|
|
109
|
+
ok: false,
|
|
110
|
+
error: buildValidationError(def, parsed.error),
|
|
111
|
+
};
|
|
112
|
+
if (atomic)
|
|
113
|
+
aborted = true;
|
|
114
|
+
return failure;
|
|
115
|
+
}
|
|
116
|
+
try {
|
|
117
|
+
const result = await runHandlerWithLimitAndRetry(def, parsed.data);
|
|
118
|
+
if (result.ok) {
|
|
119
|
+
const success = { index, ok: true, data: result.data };
|
|
120
|
+
if (atomic && def.rollback)
|
|
121
|
+
createdForRollback.push({ item: success });
|
|
122
|
+
return success;
|
|
123
|
+
}
|
|
124
|
+
const failure = {
|
|
125
|
+
index,
|
|
126
|
+
ok: false,
|
|
127
|
+
error: result.error,
|
|
128
|
+
};
|
|
129
|
+
if (atomic)
|
|
130
|
+
aborted = true;
|
|
131
|
+
return failure;
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
const failure = {
|
|
135
|
+
index,
|
|
136
|
+
ok: false,
|
|
137
|
+
error: toErrorEnvelope(error),
|
|
138
|
+
};
|
|
139
|
+
if (atomic)
|
|
140
|
+
aborted = true;
|
|
141
|
+
return failure;
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
const succeeded = results.filter((r) => r.ok).length;
|
|
145
|
+
const failed = results.length - succeeded;
|
|
146
|
+
let rolledBack;
|
|
147
|
+
if (atomic && failed > 0 && def.rollback && createdForRollback.length > 0) {
|
|
148
|
+
rolledBack = 0;
|
|
149
|
+
for (const { item } of createdForRollback) {
|
|
150
|
+
if (!item.ok)
|
|
151
|
+
continue;
|
|
152
|
+
try {
|
|
153
|
+
await def.rollback(item.data);
|
|
154
|
+
rolledBack++;
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
// best-effort: swallow rollback errors so we still return the original failure
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const batchResult = {
|
|
162
|
+
ok: failed === 0,
|
|
163
|
+
summary: { total: results.length, succeeded, failed },
|
|
164
|
+
results,
|
|
165
|
+
...(rolledBack !== undefined ? { rolled_back: rolledBack } : {}),
|
|
166
|
+
};
|
|
167
|
+
if (idempotencyKey) {
|
|
168
|
+
store(buildKey(def.name, idempotencyKey), batchResult);
|
|
169
|
+
}
|
|
170
|
+
return batchResult;
|
|
171
|
+
}
|
|
172
|
+
export const BATCH_ENVELOPE_HELP = `Batch mode: pass { items: [...], atomic?: boolean, idempotency_key?: string, concurrency?: 1-10 }. Each item is validated independently; failures are reported per-item. atomic:true forces serial execution (concurrency=1) and triggers best-effort rollback of created entities on first failure; subsequent items are skipped with code:"aborted".`;
|
|
173
|
+
export const _internal = { isBatchPayload };
|
|
174
|
+
// Re-export Zod for downstream operation files to share a single version
|
|
175
|
+
export { z };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Global limiter for Notion API calls. Notion enforces ~3 requests per second
|
|
2
|
+
// per integration; without a cap, dispatch's parallel execution (concurrency
|
|
3
|
+
// up to 10) hits 429s on larger batches. One shared instance means the limit
|
|
4
|
+
// holds across single calls, batch items, and concurrent dispatches.
|
|
5
|
+
//
|
|
6
|
+
// Implementation: a token bucket with capacity=1, equivalent to strict
|
|
7
|
+
// pacing — each acquire reserves the next slot at `1000 / rate` ms after the
|
|
8
|
+
// previous one. This guarantees the sliding-window invariant of "no more
|
|
9
|
+
// than `rate` calls in any 1-second window", whereas a bucket with capacity
|
|
10
|
+
// equal to rate would allow a 2*rate burst across the bucket-refill boundary.
|
|
11
|
+
//
|
|
12
|
+
// Rate is read from NOTION_RATE_LIMIT (requests/second). Default 3.
|
|
13
|
+
export const DEFAULT_RATE_PER_SECOND = 3;
|
|
14
|
+
export const RATE_LIMIT_ENV_VAR = "NOTION_RATE_LIMIT";
|
|
15
|
+
const MS_PER_SECOND = 1_000;
|
|
16
|
+
export class TokenBucket {
|
|
17
|
+
rate;
|
|
18
|
+
nextSlot = 0;
|
|
19
|
+
intervalMs;
|
|
20
|
+
constructor(rate) {
|
|
21
|
+
this.rate = rate;
|
|
22
|
+
if (!(rate > 0))
|
|
23
|
+
throw new Error(`TokenBucket rate must be > 0, got ${rate}`);
|
|
24
|
+
this.intervalMs = MS_PER_SECOND / rate;
|
|
25
|
+
}
|
|
26
|
+
acquire() {
|
|
27
|
+
const now = Date.now();
|
|
28
|
+
const slot = Math.max(now, this.nextSlot);
|
|
29
|
+
this.nextSlot = slot + this.intervalMs;
|
|
30
|
+
const delay = slot - now;
|
|
31
|
+
if (delay <= 0)
|
|
32
|
+
return Promise.resolve();
|
|
33
|
+
return new Promise((resolve) => setTimeout(resolve, delay));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function readRateFromEnv() {
|
|
37
|
+
const raw = process.env[RATE_LIMIT_ENV_VAR];
|
|
38
|
+
if (!raw)
|
|
39
|
+
return DEFAULT_RATE_PER_SECOND;
|
|
40
|
+
const n = Number(raw);
|
|
41
|
+
return Number.isFinite(n) && n > 0 ? n : DEFAULT_RATE_PER_SECOND;
|
|
42
|
+
}
|
|
43
|
+
let instance = new TokenBucket(readRateFromEnv());
|
|
44
|
+
export const rateLimiter = {
|
|
45
|
+
acquire() {
|
|
46
|
+
return instance.acquire();
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
// Replace the singleton bucket. Used by tests and by callers that need to
|
|
50
|
+
// re-read NOTION_RATE_LIMIT after the env var has been mutated.
|
|
51
|
+
export function configureRateLimiter(rate) {
|
|
52
|
+
instance = new TokenBucket(rate ?? readRateFromEnv());
|
|
53
|
+
}
|
|
54
|
+
export function getRateLimiterBucket() {
|
|
55
|
+
return instance;
|
|
56
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { APIErrorCode, APIResponseError, RequestTimeoutError, UnknownHTTPResponseError, } from "@notionhq/client";
|
|
2
|
+
import { isRetryableNotionCode } from "../utils/error.js";
|
|
3
|
+
const DEFAULT_ATTEMPTS = 5;
|
|
4
|
+
const DEFAULT_BASE_MS = 250;
|
|
5
|
+
const DEFAULT_MAX_MS = 8_000;
|
|
6
|
+
const SERVER_ERROR_STATUS_MIN = 500;
|
|
7
|
+
const RETRY_AFTER_HEADER = "retry-after";
|
|
8
|
+
const MS_PER_SECOND = 1_000;
|
|
9
|
+
// Re-export so dispatch and tests can classify codes against the same set
|
|
10
|
+
// without crossing modules twice.
|
|
11
|
+
export { isRetryableNotionCode as isRetryableErrorCode };
|
|
12
|
+
export function isRetryableSdkError(err) {
|
|
13
|
+
if (APIResponseError.isAPIResponseError(err)) {
|
|
14
|
+
return (err.code === APIErrorCode.RateLimited ||
|
|
15
|
+
err.status >= SERVER_ERROR_STATUS_MIN);
|
|
16
|
+
}
|
|
17
|
+
if (UnknownHTTPResponseError.isUnknownHTTPResponseError(err)) {
|
|
18
|
+
return err.status >= SERVER_ERROR_STATUS_MIN;
|
|
19
|
+
}
|
|
20
|
+
if (RequestTimeoutError.isRequestTimeoutError(err)) {
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
function parseRetryAfter(raw) {
|
|
26
|
+
if (!raw)
|
|
27
|
+
return null;
|
|
28
|
+
const seconds = Number(raw);
|
|
29
|
+
if (Number.isFinite(seconds))
|
|
30
|
+
return Math.max(0, seconds * MS_PER_SECOND);
|
|
31
|
+
const dateMs = Date.parse(raw);
|
|
32
|
+
if (!Number.isNaN(dateMs))
|
|
33
|
+
return Math.max(0, dateMs - Date.now());
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
// The SDK types `headers` as `unknown` (see fetch-types.d.ts). At runtime it's
|
|
37
|
+
// either a global `Headers` instance (native fetch) or a plain
|
|
38
|
+
// header-name → value record (legacy / test stubs). Handle both without
|
|
39
|
+
// assertions by feature-detecting.
|
|
40
|
+
function readRetryAfterHeader(headers) {
|
|
41
|
+
if (typeof Headers !== "undefined" && headers instanceof Headers) {
|
|
42
|
+
return headers.get(RETRY_AFTER_HEADER);
|
|
43
|
+
}
|
|
44
|
+
if (headers === null || typeof headers !== "object")
|
|
45
|
+
return null;
|
|
46
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
47
|
+
if (key.toLowerCase() !== RETRY_AFTER_HEADER)
|
|
48
|
+
continue;
|
|
49
|
+
const first = Array.isArray(value) ? value[0] : value;
|
|
50
|
+
return typeof first === "string" ? first : null;
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
function readRetryAfterMs(err) {
|
|
55
|
+
if (!APIResponseError.isAPIResponseError(err) &&
|
|
56
|
+
!UnknownHTTPResponseError.isUnknownHTTPResponseError(err)) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
return parseRetryAfter(readRetryAfterHeader(err.headers));
|
|
60
|
+
}
|
|
61
|
+
function computeBackoff(baseMs, maxMs, attempt, retryAfterMs) {
|
|
62
|
+
if (retryAfterMs !== null)
|
|
63
|
+
return Math.min(retryAfterMs, maxMs);
|
|
64
|
+
const ceiling = Math.min(maxMs, baseMs * 2 ** attempt);
|
|
65
|
+
return Math.random() * ceiling;
|
|
66
|
+
}
|
|
67
|
+
function sleep(ms) {
|
|
68
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
69
|
+
}
|
|
70
|
+
export async function withRetry(fn, opts = {}) {
|
|
71
|
+
const attempts = opts.attempts ?? DEFAULT_ATTEMPTS;
|
|
72
|
+
const baseMs = opts.baseMs ?? DEFAULT_BASE_MS;
|
|
73
|
+
const maxMs = opts.maxMs ?? DEFAULT_MAX_MS;
|
|
74
|
+
const shouldRetry = opts.shouldRetry ?? isRetryableSdkError;
|
|
75
|
+
const { isRetryableResult } = opts;
|
|
76
|
+
let lastErr;
|
|
77
|
+
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
78
|
+
try {
|
|
79
|
+
const result = await fn();
|
|
80
|
+
if (isRetryableResult && isRetryableResult(result)) {
|
|
81
|
+
if (attempt === attempts - 1)
|
|
82
|
+
return result;
|
|
83
|
+
await sleep(computeBackoff(baseMs, maxMs, attempt, null));
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
lastErr = err;
|
|
90
|
+
if (attempt === attempts - 1 || !shouldRetry(err))
|
|
91
|
+
throw err;
|
|
92
|
+
await sleep(computeBackoff(baseMs, maxMs, attempt, readRetryAfterMs(err)));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// Unreachable: the loop either returns or throws on the final attempt.
|
|
96
|
+
throw lastErr;
|
|
97
|
+
}
|
package/build/index.js
CHANGED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { unified } from "unified";
|
|
2
|
+
import remarkParse from "remark-parse";
|
|
3
|
+
import remarkGfm from "remark-gfm";
|
|
4
|
+
const processor = unified().use(remarkParse).use(remarkGfm);
|
|
5
|
+
export function parseMarkdownToBlocks(markdown) {
|
|
6
|
+
if (!markdown.trim())
|
|
7
|
+
return [];
|
|
8
|
+
const tree = processor.parse(markdown);
|
|
9
|
+
return tree.children.flatMap(convertBlock);
|
|
10
|
+
}
|
|
11
|
+
function convertBlock(node) {
|
|
12
|
+
switch (node.type) {
|
|
13
|
+
case "paragraph":
|
|
14
|
+
return [paragraphFrom(node)];
|
|
15
|
+
case "heading":
|
|
16
|
+
return [headingFrom(node)];
|
|
17
|
+
case "list":
|
|
18
|
+
return convertList(node);
|
|
19
|
+
case "blockquote":
|
|
20
|
+
return convertBlockquote(node);
|
|
21
|
+
case "code":
|
|
22
|
+
return [codeFrom(node)];
|
|
23
|
+
case "thematicBreak":
|
|
24
|
+
return [{ type: "divider", divider: {} }];
|
|
25
|
+
default:
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function paragraphFrom(node) {
|
|
30
|
+
if (node.children.length === 1 && node.children[0].type === "image") {
|
|
31
|
+
return imageFrom(node.children[0]);
|
|
32
|
+
}
|
|
33
|
+
const inlineImage = node.children.find((c) => c.type === "image");
|
|
34
|
+
if (inlineImage && node.children.length === 1) {
|
|
35
|
+
return imageFrom(inlineImage);
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
type: "paragraph",
|
|
39
|
+
paragraph: { rich_text: phrasingToRichText(node.children) },
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function headingFrom(node) {
|
|
43
|
+
const level = Math.min(4, node.depth);
|
|
44
|
+
const key = `heading_${level}`;
|
|
45
|
+
return {
|
|
46
|
+
type: key,
|
|
47
|
+
[key]: { rich_text: phrasingToRichText(node.children) },
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function convertList(node) {
|
|
51
|
+
const isOrdered = node.ordered === true;
|
|
52
|
+
const type = isOrdered ? "numbered_list_item" : "bulleted_list_item";
|
|
53
|
+
return node.children.map((item) => listItemBlock(item, type));
|
|
54
|
+
}
|
|
55
|
+
function listItemBlock(item, defaultType) {
|
|
56
|
+
const isToDo = typeof item.checked === "boolean";
|
|
57
|
+
const firstParaIdx = item.children.findIndex((c) => c.type === "paragraph");
|
|
58
|
+
const head = firstParaIdx >= 0
|
|
59
|
+
? item.children[firstParaIdx]
|
|
60
|
+
: { type: "paragraph", children: [] };
|
|
61
|
+
const richText = phrasingToRichText(head.children);
|
|
62
|
+
const tail = item.children.filter((_, i) => i !== firstParaIdx);
|
|
63
|
+
const children = tail.flatMap(convertBlock);
|
|
64
|
+
if (isToDo) {
|
|
65
|
+
return {
|
|
66
|
+
type: "to_do",
|
|
67
|
+
to_do: {
|
|
68
|
+
rich_text: richText,
|
|
69
|
+
checked: item.checked === true,
|
|
70
|
+
...(children.length ? { children } : {}),
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
type: defaultType,
|
|
76
|
+
[defaultType]: {
|
|
77
|
+
rich_text: richText,
|
|
78
|
+
...(children.length ? { children } : {}),
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function convertBlockquote(node) {
|
|
83
|
+
const children = node.children.flatMap(convertBlock);
|
|
84
|
+
if (children.length === 0)
|
|
85
|
+
return [];
|
|
86
|
+
const [first, ...rest] = children;
|
|
87
|
+
if (first.type === "paragraph") {
|
|
88
|
+
const paragraph = first.paragraph;
|
|
89
|
+
return [
|
|
90
|
+
{
|
|
91
|
+
type: "quote",
|
|
92
|
+
quote: {
|
|
93
|
+
rich_text: paragraph.rich_text,
|
|
94
|
+
...(rest.length ? { children: rest } : {}),
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
];
|
|
98
|
+
}
|
|
99
|
+
return [
|
|
100
|
+
{
|
|
101
|
+
type: "quote",
|
|
102
|
+
quote: { rich_text: [], children },
|
|
103
|
+
},
|
|
104
|
+
];
|
|
105
|
+
}
|
|
106
|
+
function codeFrom(node) {
|
|
107
|
+
return {
|
|
108
|
+
type: "code",
|
|
109
|
+
code: {
|
|
110
|
+
rich_text: [
|
|
111
|
+
{
|
|
112
|
+
type: "text",
|
|
113
|
+
text: { content: node.value },
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
language: normalizeLanguage(node.lang),
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
function imageFrom(node) {
|
|
121
|
+
return {
|
|
122
|
+
type: "image",
|
|
123
|
+
image: {
|
|
124
|
+
type: "external",
|
|
125
|
+
external: { url: node.url },
|
|
126
|
+
...(node.alt
|
|
127
|
+
? {
|
|
128
|
+
caption: [
|
|
129
|
+
{ type: "text", text: { content: node.alt } },
|
|
130
|
+
],
|
|
131
|
+
}
|
|
132
|
+
: {}),
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
function phrasingToRichText(nodes, annotations = {}) {
|
|
137
|
+
const out = [];
|
|
138
|
+
for (const node of nodes) {
|
|
139
|
+
pushPhrasing(node, annotations, out);
|
|
140
|
+
}
|
|
141
|
+
return mergeAdjacent(out);
|
|
142
|
+
}
|
|
143
|
+
function pushPhrasing(node, annotations, out) {
|
|
144
|
+
switch (node.type) {
|
|
145
|
+
case "text":
|
|
146
|
+
out.push(textRT(node.value, annotations));
|
|
147
|
+
return;
|
|
148
|
+
case "strong":
|
|
149
|
+
for (const c of node.children) {
|
|
150
|
+
pushPhrasing(c, { ...annotations, bold: true }, out);
|
|
151
|
+
}
|
|
152
|
+
return;
|
|
153
|
+
case "emphasis":
|
|
154
|
+
for (const c of node.children) {
|
|
155
|
+
pushPhrasing(c, { ...annotations, italic: true }, out);
|
|
156
|
+
}
|
|
157
|
+
return;
|
|
158
|
+
case "delete":
|
|
159
|
+
for (const c of node.children) {
|
|
160
|
+
pushPhrasing(c, { ...annotations, strikethrough: true }, out);
|
|
161
|
+
}
|
|
162
|
+
return;
|
|
163
|
+
case "inlineCode":
|
|
164
|
+
out.push(textRT(node.value, { ...annotations, code: true }));
|
|
165
|
+
return;
|
|
166
|
+
case "link": {
|
|
167
|
+
const link = node;
|
|
168
|
+
const inner = [];
|
|
169
|
+
for (const c of link.children)
|
|
170
|
+
pushPhrasing(c, annotations, inner);
|
|
171
|
+
const merged = mergeAdjacent(inner);
|
|
172
|
+
for (const r of merged) {
|
|
173
|
+
if (r.type === "text")
|
|
174
|
+
r.text.link = { url: link.url };
|
|
175
|
+
out.push(r);
|
|
176
|
+
}
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
case "break":
|
|
180
|
+
out.push(textRT("\n", annotations));
|
|
181
|
+
return;
|
|
182
|
+
default:
|
|
183
|
+
// Unhandled inline (image inside paragraph already promoted; html, footnote, etc.)
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
function textRT(content, annotations) {
|
|
188
|
+
const cleaned = clean(annotations);
|
|
189
|
+
return {
|
|
190
|
+
type: "text",
|
|
191
|
+
text: { content },
|
|
192
|
+
...(Object.keys(cleaned).length ? { annotations: cleaned } : {}),
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
function clean(a) {
|
|
196
|
+
const out = {};
|
|
197
|
+
if (a.bold)
|
|
198
|
+
out.bold = true;
|
|
199
|
+
if (a.italic)
|
|
200
|
+
out.italic = true;
|
|
201
|
+
if (a.strikethrough)
|
|
202
|
+
out.strikethrough = true;
|
|
203
|
+
if (a.underline)
|
|
204
|
+
out.underline = true;
|
|
205
|
+
if (a.code)
|
|
206
|
+
out.code = true;
|
|
207
|
+
return out;
|
|
208
|
+
}
|
|
209
|
+
function mergeAdjacent(items) {
|
|
210
|
+
const out = [];
|
|
211
|
+
for (const item of items) {
|
|
212
|
+
const prev = out[out.length - 1];
|
|
213
|
+
if (prev &&
|
|
214
|
+
prev.type === "text" &&
|
|
215
|
+
item.type === "text" &&
|
|
216
|
+
sameAnnotations(prev.annotations, item.annotations) &&
|
|
217
|
+
sameLink(prev.text.link, item.text.link)) {
|
|
218
|
+
prev.text.content += item.text.content;
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
out.push(item);
|
|
222
|
+
}
|
|
223
|
+
return out;
|
|
224
|
+
}
|
|
225
|
+
function sameAnnotations(a, b) {
|
|
226
|
+
return JSON.stringify(a ?? {}) === JSON.stringify(b ?? {});
|
|
227
|
+
}
|
|
228
|
+
function sameLink(a, b) {
|
|
229
|
+
return (a?.url ?? null) === (b?.url ?? null);
|
|
230
|
+
}
|
|
231
|
+
const KNOWN_LANGUAGES = new Set([
|
|
232
|
+
"abap", "arduino", "bash", "basic", "c", "clojure", "coffeescript", "c++",
|
|
233
|
+
"c#", "css", "dart", "diff", "docker", "elixir", "elm", "erlang", "flow",
|
|
234
|
+
"fortran", "f#", "gherkin", "glsl", "go", "graphql", "groovy", "haskell",
|
|
235
|
+
"html", "java", "javascript", "json", "julia", "kotlin", "latex", "less",
|
|
236
|
+
"lisp", "livescript", "lua", "makefile", "markdown", "markup", "matlab",
|
|
237
|
+
"mermaid", "nix", "objective-c", "ocaml", "pascal", "perl", "php", "plain text",
|
|
238
|
+
"powershell", "prolog", "protobuf", "python", "r", "reason", "ruby", "rust",
|
|
239
|
+
"sass", "scala", "scheme", "scss", "shell", "sql", "swift", "typescript",
|
|
240
|
+
"vb.net", "verilog", "vhdl", "visual basic", "webassembly", "xml", "yaml",
|
|
241
|
+
]);
|
|
242
|
+
function normalizeLanguage(lang) {
|
|
243
|
+
if (!lang)
|
|
244
|
+
return "plain text";
|
|
245
|
+
const lower = lang.toLowerCase();
|
|
246
|
+
if (KNOWN_LANGUAGES.has(lower))
|
|
247
|
+
return lower;
|
|
248
|
+
const aliases = {
|
|
249
|
+
js: "javascript",
|
|
250
|
+
ts: "typescript",
|
|
251
|
+
py: "python",
|
|
252
|
+
rb: "ruby",
|
|
253
|
+
sh: "shell",
|
|
254
|
+
yml: "yaml",
|
|
255
|
+
md: "markdown",
|
|
256
|
+
"c++": "c++",
|
|
257
|
+
cpp: "c++",
|
|
258
|
+
cs: "c#",
|
|
259
|
+
"objc": "objective-c",
|
|
260
|
+
rs: "rust",
|
|
261
|
+
tsx: "typescript",
|
|
262
|
+
jsx: "javascript",
|
|
263
|
+
};
|
|
264
|
+
return aliases[lower] ?? "plain text";
|
|
265
|
+
}
|