@vercel/board 0.0.0-stage → 0.15.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/README.md +179 -2
- package/dist/a2-5EG8FkZ8.d.mts +3359 -0
- package/dist/a2-DYGYankn.mjs +3045 -0
- package/dist/access-CJU2-vaa.mjs +226 -0
- package/dist/access-CV96SYgr.d.mts +32 -0
- package/dist/access.d.mts +29 -0
- package/dist/access.mjs +4 -0
- package/dist/actors-rdN4dDTU.mjs +223 -0
- package/dist/attempts-BQUp9MKO.mjs +919 -0
- package/dist/auth-COzc09sn.mjs +276 -0
- package/dist/auth-wHFcAte3.d.mts +203 -0
- package/dist/backend-a2/blob-vercel.d.mts +8 -0
- package/dist/backend-a2/blob-vercel.mjs +81 -0
- package/dist/backend-a2/contract.d.mts +18 -0
- package/dist/backend-a2/contract.mjs +5 -0
- package/dist/backend-a2/scheduler-local.d.mts +11 -0
- package/dist/backend-a2/scheduler-local.mjs +2 -0
- package/dist/backend-a2/scheduler-qstash.d.mts +2 -0
- package/dist/backend-a2/scheduler-qstash.mjs +2 -0
- package/dist/backend-a2/scheduler-vercel.d.mts +2 -0
- package/dist/backend-a2/scheduler-vercel.mjs +2 -0
- package/dist/backend-a2/store-memory.d.mts +11 -0
- package/dist/backend-a2/store-memory.mjs +19 -0
- package/dist/backend-a2/store-postgres.d.mts +38 -0
- package/dist/backend-a2/store-postgres.mjs +110 -0
- package/dist/backend-a2/store-redis-http.d.mts +17 -0
- package/dist/backend-a2/store-redis-http.mjs +32 -0
- package/dist/backend-a2/store-redis.d.mts +23 -0
- package/dist/backend-a2/store-redis.mjs +39 -0
- package/dist/backend-a2/store-sqlite.d.mts +12 -0
- package/dist/backend-a2/store-sqlite.mjs +31 -0
- package/dist/backend-a2.d.mts +4 -0
- package/dist/backend-a2.mjs +2 -0
- package/dist/backend-http-8ex5g7Wv.d.mts +630 -0
- package/dist/backend-http-BsDSgmeo.mjs +613 -0
- package/dist/backend-http-Cv6xbEy9.mjs +469 -0
- package/dist/backend-http.d.mts +10 -0
- package/dist/backend-http.mjs +2 -0
- package/dist/backend-http.node.d.mts +10 -0
- package/dist/backend-http.node.mjs +2 -0
- package/dist/blob-C0J3wbGv.d.mts +39 -0
- package/dist/bots-CGkO76qH.mjs +67 -0
- package/dist/chat-provider-XCnQlxq6.d.mts +140 -0
- package/dist/chat-provider-jFSeyNmB.mjs +389 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +2252 -0
- package/dist/client-DGDPaN7U.mjs +380 -0
- package/dist/conformance-DRR23YNy.mjs +512 -0
- package/dist/conformance.d.mts +30 -0
- package/dist/conformance.mjs +2 -0
- package/dist/contract-CuJmlrhw.mjs +566 -0
- package/dist/emoji-CTLpvH-A.mjs +445 -0
- package/dist/engine-3EvQ8idk.mjs +1449 -0
- package/dist/errors-DX3hpwWt.mjs +64 -0
- package/dist/exports-BpgyEdh6.d.mts +4 -0
- package/dist/exports-CyY3_FjZ.mjs +1285 -0
- package/dist/files-SniSmeEr.mjs +47 -0
- package/dist/index.d.mts +13 -0
- package/dist/index.default-BZ3lItjH.mjs +12 -0
- package/dist/index.mjs +19 -0
- package/dist/index.node.d.mts +14 -0
- package/dist/index.node.mjs +5 -0
- package/dist/inspect/app.js +416 -0
- package/dist/inspect/engine.js +5 -0
- package/dist/local-Cr_dSjvs.mjs +44 -0
- package/dist/own-OwgPWdmh.mjs +15 -0
- package/dist/project-CFiArraX.mjs +124 -0
- package/dist/provider-BGjz-mDa.d.mts +2473 -0
- package/dist/provider-CuewcDNZ.mjs +205 -0
- package/dist/redis-tables-B35vv4CZ.mjs +842 -0
- package/dist/reducers-B_vUGQRp.d.mts +1172 -0
- package/dist/reducers-mUPFuFJV.mjs +293 -0
- package/dist/schemas-Be08nyfW.mjs +74 -0
- package/dist/schemas-CWxs3ECJ.d.mts +50 -0
- package/dist/serve-Ccbb2LSb.mjs +170 -0
- package/dist/server-Cxey38B-.mjs +17 -0
- package/dist/serving-BNbzc96L.mjs +2 -0
- package/dist/sql-DbiEnqM0.mjs +571 -0
- package/dist/sqlite-db-Cbj03vmp.mjs +70 -0
- package/dist/sync-github.d.mts +473 -0
- package/dist/sync-github.mjs +603 -0
- package/dist/sync-kapso.d.mts +175 -0
- package/dist/sync-kapso.mjs +86 -0
- package/dist/sync-slack.d.mts +364 -0
- package/dist/sync-slack.mjs +728 -0
- package/dist/sync.chat.d.mts +2 -0
- package/dist/sync.chat.mjs +2 -0
- package/dist/sync.d.mts +3 -0
- package/dist/sync.mjs +4 -0
- package/dist/targets-ojFEXEyN.mjs +94 -0
- package/dist/tokens-DKLn5P9B.mjs +256 -0
- package/dist/types-BHCjTZdB.d.mts +104 -0
- package/dist/users-Dls7_ANV.mjs +42 -0
- package/dist/watch-spec-W0c7ypHJ.mjs +787 -0
- package/dist/watchers-hash-D9hbHLKB.mjs +225 -0
- package/docs/01-board/01-mounting.md +506 -0
- package/docs/01-board/02-database.md +258 -0
- package/docs/01-board/03-identity.md +262 -0
- package/docs/01-board/04-messages.md +192 -0
- package/docs/01-board/05-reading.md +177 -0
- package/docs/01-board/06-watchers.md +249 -0
- package/docs/01-board/07-schedules.md +133 -0
- package/docs/01-board/08-middleware.md +139 -0
- package/docs/01-board/09-access.md +499 -0
- package/docs/01-quickstart.md +196 -0
- package/docs/02-sync/01-overview.md +290 -0
- package/docs/02-sync/02-callbacks.md +265 -0
- package/docs/02-sync/03-slack.md +248 -0
- package/docs/02-sync/04-github.md +201 -0
- package/docs/02-sync/05-whatsapp.md +148 -0
- package/docs/02-sync/06-providers.md +228 -0
- package/docs/03-guides/01-coding-agents.md +226 -0
- package/docs/03-guides/02-inspector.md +104 -0
- package/docs/03-guides/03-ci.md +251 -0
- package/docs/03-guides/04-triage.md +233 -0
- package/docs/03-guides/05-agent-factory.md +142 -0
- package/docs/04-reference/01-cli.md +606 -0
- package/docs/04-reference/02-board.md +579 -0
- package/docs/04-reference/03-http.md +489 -0
- package/docs/04-reference/04-log.md +250 -0
- package/docs/04-reference/05-conformance.md +97 -0
- package/docs/index.md +186 -0
- package/package.json +165 -3
- package/sql/postgres/board.sql +145 -0
- package/sql/postgres/notifications.sql +24 -0
- package/sql/postgres/schema.sql +451 -0
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
import { a as typeOf, n as DIRECT_UPLOAD_BYTES_MAX, t as ATTACHMENT_BYTES_MAX } from "./files-SniSmeEr.mjs";
|
|
2
|
+
import { r as normalizeEmoji } from "./emoji-CTLpvH-A.mjs";
|
|
3
|
+
//#region src/client/sse.ts
|
|
4
|
+
/** Parses a text/event-stream body into messages. */
|
|
5
|
+
async function* parseSse(body, signal) {
|
|
6
|
+
const reader = body.getReader();
|
|
7
|
+
const decoder = new TextDecoder();
|
|
8
|
+
let buffer = "";
|
|
9
|
+
const onAbort = () => void reader.cancel().catch(() => {});
|
|
10
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
11
|
+
try {
|
|
12
|
+
while (true) {
|
|
13
|
+
const { value, done } = await reader.read();
|
|
14
|
+
if (done) return;
|
|
15
|
+
buffer += decoder.decode(value, { stream: true });
|
|
16
|
+
let boundary;
|
|
17
|
+
while ((boundary = buffer.search(/\r?\n\r?\n/)) !== -1) {
|
|
18
|
+
const block = buffer.slice(0, boundary);
|
|
19
|
+
buffer = buffer.slice(boundary).replace(/^\r?\n\r?\n/, "");
|
|
20
|
+
const message = {};
|
|
21
|
+
const data = [];
|
|
22
|
+
for (const line of block.split(/\r?\n/)) {
|
|
23
|
+
if (line === "" || line.startsWith(":")) continue;
|
|
24
|
+
const colon = line.indexOf(":");
|
|
25
|
+
const field = colon === -1 ? line : line.slice(0, colon);
|
|
26
|
+
const value = colon === -1 ? "" : line.slice(colon + 1).replace(/^ /, "");
|
|
27
|
+
if (field === "data") data.push(value);
|
|
28
|
+
else if (field === "id") message.id = value;
|
|
29
|
+
else if (field === "event") message.event = value;
|
|
30
|
+
}
|
|
31
|
+
if (data.length > 0) message.data = data.join("\n");
|
|
32
|
+
if (message.id !== void 0 || message.data !== void 0) yield message;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
} finally {
|
|
36
|
+
signal?.removeEventListener("abort", onAbort);
|
|
37
|
+
reader.releaseLock();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
//#endregion
|
|
41
|
+
//#region src/client/client.ts
|
|
42
|
+
var BoardError = class extends Error {
|
|
43
|
+
code;
|
|
44
|
+
status;
|
|
45
|
+
constructor(code, message, status) {
|
|
46
|
+
super(message);
|
|
47
|
+
this.name = "BoardError";
|
|
48
|
+
this.code = code;
|
|
49
|
+
this.status = status;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
const resolve = async (value) => typeof value === "function" ? await value() : value;
|
|
53
|
+
const scheduleBody = (input) => {
|
|
54
|
+
const { at, until, ...rest } = messageBody(input);
|
|
55
|
+
return {
|
|
56
|
+
...rest,
|
|
57
|
+
...at !== void 0 ? { at: at instanceof Date ? at.toISOString() : at } : {},
|
|
58
|
+
...until !== void 0 ? { until: until instanceof Date ? until.toISOString() : until } : {}
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
const messageBody = (input) => {
|
|
62
|
+
if (typeof input === "string") return { text: input };
|
|
63
|
+
const { attachments, ...rest } = input;
|
|
64
|
+
return attachments?.length ? {
|
|
65
|
+
...rest,
|
|
66
|
+
attachments: attachments.map((a) => typeof a === "string" ? a : a.id)
|
|
67
|
+
} : rest;
|
|
68
|
+
};
|
|
69
|
+
const UNITS = {
|
|
70
|
+
ms: 1,
|
|
71
|
+
s: 1e3,
|
|
72
|
+
m: 6e4,
|
|
73
|
+
h: 36e5,
|
|
74
|
+
d: 864e5
|
|
75
|
+
};
|
|
76
|
+
function duration(value) {
|
|
77
|
+
if (typeof value === "number") return value;
|
|
78
|
+
const match = value.trim().match(/^(\d+(?:\.\d+)?)(ms|s|m|h|d)?$/);
|
|
79
|
+
if (!match) throw new BoardError("invalid_duration", `invalid duration: ${value}`, 0);
|
|
80
|
+
return Number(match[1]) * UNITS[match[2] ?? "ms"];
|
|
81
|
+
}
|
|
82
|
+
/** A ref as a path segment: opaque, so it's encoded as it is. */
|
|
83
|
+
const path = (id) => encodeURIComponent(id);
|
|
84
|
+
function queryString(query) {
|
|
85
|
+
const params = new URLSearchParams();
|
|
86
|
+
for (const [key, value] of Object.entries(query)) {
|
|
87
|
+
if (value === void 0 || value === null) continue;
|
|
88
|
+
if (key === "label") {
|
|
89
|
+
const labels = Array.isArray(value) ? value : typeof value === "object" ? Object.entries(value).map(([k, v]) => `${k}=${String(v)}`) : [value];
|
|
90
|
+
for (const label of labels) params.append("label", String(label));
|
|
91
|
+
} else params.set(key, String(value));
|
|
92
|
+
}
|
|
93
|
+
const text = params.toString();
|
|
94
|
+
return text ? `?${text}` : "";
|
|
95
|
+
}
|
|
96
|
+
const written = (value, unknownMentions) => unknownMentions?.length ? {
|
|
97
|
+
...value,
|
|
98
|
+
unknownMentions
|
|
99
|
+
} : value;
|
|
100
|
+
function createBoardClient(options) {
|
|
101
|
+
const base = options.url.replace(/\/+$/, "");
|
|
102
|
+
const doFetch = options.fetch ?? ((input, init) => globalThis.fetch(input, init));
|
|
103
|
+
const storeFetch = options.storeFetch ?? ((input, init) => globalThis.fetch(input, init));
|
|
104
|
+
async function headers(auth, as) {
|
|
105
|
+
const out = { ...options.headers ? await resolve(options.headers) : {} };
|
|
106
|
+
if (!auth) return out;
|
|
107
|
+
if (options.token !== void 0) out.authorization = `Bearer ${await resolve(options.token)}`;
|
|
108
|
+
const profile = as !== void 0 ? as : options.profile === void 0 ? null : await resolve(options.profile);
|
|
109
|
+
if (profile !== null) {
|
|
110
|
+
const { name, avatar } = typeof profile === "string" ? {
|
|
111
|
+
name: profile,
|
|
112
|
+
avatar: void 0
|
|
113
|
+
} : profile;
|
|
114
|
+
out["x-board-profile"] = name;
|
|
115
|
+
if (avatar) out["x-board-avatar"] = avatar;
|
|
116
|
+
} else {
|
|
117
|
+
const person = options.person === void 0 ? null : await resolve(options.person);
|
|
118
|
+
if (person?.name) out["x-board-name"] = encodeURIComponent(person.name);
|
|
119
|
+
if (person?.avatar) out["x-board-avatar"] = person.avatar;
|
|
120
|
+
}
|
|
121
|
+
const session = as !== void 0 ? null : options.session === void 0 ? null : await resolve(options.session);
|
|
122
|
+
if (session) out["x-board-session"] = session;
|
|
123
|
+
const handle = options.handle === void 0 ? null : await resolve(options.handle);
|
|
124
|
+
if (handle) out["x-board-handle"] = handle;
|
|
125
|
+
return out;
|
|
126
|
+
}
|
|
127
|
+
async function raw(method, path, init = {}) {
|
|
128
|
+
const response = await doFetch(`${base}${path}`, {
|
|
129
|
+
method,
|
|
130
|
+
headers: {
|
|
131
|
+
...await headers(init.auth ?? true),
|
|
132
|
+
...init.body === void 0 ? {} : { "content-type": "application/json" },
|
|
133
|
+
...init.headers
|
|
134
|
+
},
|
|
135
|
+
...init.body === void 0 ? {} : { body: JSON.stringify(init.body) },
|
|
136
|
+
...init.signal ? { signal: init.signal } : {}
|
|
137
|
+
});
|
|
138
|
+
if (!response.ok) throw await toError(response);
|
|
139
|
+
return response;
|
|
140
|
+
}
|
|
141
|
+
async function call(method, path, init) {
|
|
142
|
+
return await (await raw(method, path, init)).json();
|
|
143
|
+
}
|
|
144
|
+
const filtersQuery = (filters = {}) => filters;
|
|
145
|
+
async function poll(query = {}, signal) {
|
|
146
|
+
return call("GET", `/events${queryString(query)}`, signal ? { signal } : {});
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
url: base,
|
|
150
|
+
/**
|
|
151
|
+
* The board's HTTP API with this client's credentials, and the response as
|
|
152
|
+
* the board sent it, errors included: for proxies like `vb inspect`.
|
|
153
|
+
*/
|
|
154
|
+
request: async (path, init = {}) => doFetch(`${base}${path}`, {
|
|
155
|
+
method: init.method ?? "GET",
|
|
156
|
+
headers: {
|
|
157
|
+
...init.headers,
|
|
158
|
+
...await headers(true, init.as)
|
|
159
|
+
},
|
|
160
|
+
...init.body ? { body: init.body } : {},
|
|
161
|
+
...init.signal ? { signal: init.signal } : {}
|
|
162
|
+
}),
|
|
163
|
+
discovery: () => call("GET", "/", { auth: false }),
|
|
164
|
+
me: () => call("GET", "/me"),
|
|
165
|
+
channels: async () => (await call("GET", "/channels")).channels,
|
|
166
|
+
actors: async () => (await call("GET", "/actors")).actors,
|
|
167
|
+
/** Your linked platform accounts, and the sync entries you haven't linked (`unlinked`). */
|
|
168
|
+
identity: () => call("GET", "/identity"),
|
|
169
|
+
/** Unlinks one of your accounts, by actor ID or handle. History keeps its messages as they were. */
|
|
170
|
+
unlinkAccount: (account) => call("POST", "/identity/unlink", { body: { actor: account } }),
|
|
171
|
+
messages: (query = {}) => call("GET", `/messages${queryString(query)}`),
|
|
172
|
+
thread: (id) => call("GET", `/messages/${path(id)}`),
|
|
173
|
+
/** Your own post with the client ID you gave it, as posting it answered: 404 `post_not_found` when there's none. */
|
|
174
|
+
posted: async (id) => (await call("GET", `/posts/${path(id)}`)).message,
|
|
175
|
+
/** Threads with a matching message, one summary each (root, reply count, latest reply), by latest activity. `limit` counts threads. */
|
|
176
|
+
threads: (query = {}) => call("GET", `/threads${queryString(query)}`),
|
|
177
|
+
/** `unknownMentions` lists the handles that matched nobody: they posted as plain text. */
|
|
178
|
+
async post(channel, input) {
|
|
179
|
+
const { message, unknownMentions } = await call("POST", "/messages", { body: {
|
|
180
|
+
channel,
|
|
181
|
+
...messageBody(input)
|
|
182
|
+
} });
|
|
183
|
+
return written(message, unknownMentions);
|
|
184
|
+
},
|
|
185
|
+
async reply(id, input) {
|
|
186
|
+
const { message, unknownMentions } = await call("POST", "/messages", { body: {
|
|
187
|
+
re: id,
|
|
188
|
+
...messageBody(input)
|
|
189
|
+
} });
|
|
190
|
+
return written(message, unknownMentions);
|
|
191
|
+
},
|
|
192
|
+
/**
|
|
193
|
+
* Posts later, as you: a new thread when `target` is a channel, a reply when it's
|
|
194
|
+
* `{ re }`, any message's ID in the thread. Each message it posts carries
|
|
195
|
+
* `schedule: { id, n }`. Needs a board with a scheduler.
|
|
196
|
+
*/
|
|
197
|
+
async schedule(target, input) {
|
|
198
|
+
const { schedule, unknownMentions } = await call("POST", "/schedules", { body: {
|
|
199
|
+
...typeof target === "string" ? { channel: target } : { re: target.re },
|
|
200
|
+
...scheduleBody(input)
|
|
201
|
+
} });
|
|
202
|
+
return written(schedule, unknownMentions);
|
|
203
|
+
},
|
|
204
|
+
/** Pending schedules (`all` for done and canceled too), oldest first. `re` is any message in a thread. */
|
|
205
|
+
schedules: async (query = {}) => (await call("GET", `/schedules${queryString(query)}`)).schedules,
|
|
206
|
+
/** Stops a schedule: yours, or any with manage in its channel. `canceled` is false when it was already done or canceled. */
|
|
207
|
+
cancelSchedule: (id) => call("POST", `/schedules/${path(id)}/cancel`, { body: {} }),
|
|
208
|
+
/**
|
|
209
|
+
* Stores a file on the board, for a post or reply's `attachments`. Up to 4 MB goes in one
|
|
210
|
+
* request; bigger, up to 100 MB, goes straight to the board's blob store on a URL the board
|
|
211
|
+
* signs, where its store can (`no_direct_uploads` where it can't).
|
|
212
|
+
*/
|
|
213
|
+
async upload(input) {
|
|
214
|
+
const type = input.type || (input.data instanceof Blob ? input.data.type : "") || typeOf(input.name);
|
|
215
|
+
const size = input.data instanceof Blob ? input.data.size : input.data.byteLength;
|
|
216
|
+
if (size > 4194304) {
|
|
217
|
+
if (size > 104857600) throw new BoardError("too_large", `attachments are at most ${DIRECT_UPLOAD_BYTES_MAX / 1024 / 1024} MB`, 413);
|
|
218
|
+
const ticket = await call("POST", "/attachments/uploads", { body: {
|
|
219
|
+
name: input.name,
|
|
220
|
+
type,
|
|
221
|
+
size
|
|
222
|
+
} }).catch((error) => {
|
|
223
|
+
if (error instanceof BoardError && error.status === 404 && error.code === "not_found") throw new BoardError("too_large", `this board takes files of at most ${ATTACHMENT_BYTES_MAX / 1024 / 1024} MB`, 413);
|
|
224
|
+
throw error;
|
|
225
|
+
});
|
|
226
|
+
const put = await storeFetch(ticket.url, {
|
|
227
|
+
method: "PUT",
|
|
228
|
+
headers: ticket.headers,
|
|
229
|
+
body: input.data
|
|
230
|
+
});
|
|
231
|
+
if (!put.ok) throw new BoardError("upload_failed", `the blob store refused the file: ${put.status} ${(await put.text().catch(() => "")).slice(0, 300) || put.statusText}`, put.status);
|
|
232
|
+
return (await call("POST", `/attachments/uploads/${encodeURIComponent(ticket.id)}`, { body: {} })).attachment;
|
|
233
|
+
}
|
|
234
|
+
const response = await doFetch(`${base}/attachments`, {
|
|
235
|
+
method: "POST",
|
|
236
|
+
headers: {
|
|
237
|
+
...await headers(true),
|
|
238
|
+
"content-type": type,
|
|
239
|
+
"x-board-filename": encodeURIComponent(input.name)
|
|
240
|
+
},
|
|
241
|
+
body: input.data
|
|
242
|
+
});
|
|
243
|
+
if (!response.ok) throw await toError(response);
|
|
244
|
+
return (await response.json()).attachment;
|
|
245
|
+
},
|
|
246
|
+
/** An attachment's bytes, as the board serves them: `content-type` is the file's type. */
|
|
247
|
+
download: (id, opts = {}) => raw("GET", `/attachments/${encodeURIComponent(id)}`, opts.signal ? { signal: opts.signal } : {}),
|
|
248
|
+
edit: (id, text) => call("POST", `/messages/${path(id)}/edit`, { body: { text } }),
|
|
249
|
+
delete: (id) => call("POST", `/messages/${path(id)}/delete`, { body: {} }),
|
|
250
|
+
/** `emoji` is a short name, `:name:`, or the character itself. */
|
|
251
|
+
react: (id, emoji, opts = {}) => call("POST", `/messages/${path(id)}/reactions`, { body: {
|
|
252
|
+
emoji: normalizeEmoji(emoji),
|
|
253
|
+
...opts
|
|
254
|
+
} }),
|
|
255
|
+
label: (id, change) => call("POST", `/messages/${path(id)}/labels`, { body: change }),
|
|
256
|
+
/**
|
|
257
|
+
* Shows typing in the platform threads the message's thread is synced to
|
|
258
|
+
* (on WhatsApp, also marking the latest inbound message read). Ephemeral:
|
|
259
|
+
* nothing goes in the log. Answers the external threads that took it.
|
|
260
|
+
* `status` replaces "Typing…" on Slack ("running tests…").
|
|
261
|
+
*/
|
|
262
|
+
typing: async (id, status) => (await call("POST", `/messages/${path(id)}/typing`, { body: status ? { status } : {} })).targets,
|
|
263
|
+
/** Attaches the message's thread to an external target, backfilling it. Attaching twice is a no-op. */
|
|
264
|
+
sync: (id, target) => call("POST", `/messages/${path(id)}/sync`, { body: { target } }),
|
|
265
|
+
/** Raw platform events for a sync entry, newest first. */
|
|
266
|
+
inbox: async (key, opts = {}) => (await call("GET", `/inbox/${key}${queryString(opts)}`)).events,
|
|
267
|
+
/** Registers the caller's watchers, replacing its last registration: what a board on `http()` does. */
|
|
268
|
+
register: (input) => call("POST", "/watchers", { body: input }),
|
|
269
|
+
/** Registered watchers: every one to `manage` everywhere, the caller's own to anyone else. */
|
|
270
|
+
watchers: async () => (await call("GET", "/watchers")).watchers,
|
|
271
|
+
/** The caller's matches it can run now, each leased to it until it acks. `wait` holds the request up to that many seconds for one. */
|
|
272
|
+
matches: async (opts = {}) => {
|
|
273
|
+
const { signal, ...query } = opts;
|
|
274
|
+
return (await call("GET", `/watchers/matches${queryString(query)}`, signal ? { signal } : {})).matches;
|
|
275
|
+
},
|
|
276
|
+
/** How a pulled match went: done, an error to try again, or `skip` to drop it. */
|
|
277
|
+
ack: (id, body) => call("POST", `/watchers/matches/${encodeURIComponent(id)}/ack`, { body }),
|
|
278
|
+
/**
|
|
279
|
+
* Reads a platform channel (`#name`, an ID), thread (`C0123:1727000000.123456`),
|
|
280
|
+
* or message permalink on demand, without syncing or storing it. Full access only.
|
|
281
|
+
*/
|
|
282
|
+
readSource: (key, target, opts = {}) => call("GET", `/sources/${key}/read${queryString({
|
|
283
|
+
target,
|
|
284
|
+
...opts
|
|
285
|
+
})}`),
|
|
286
|
+
poll,
|
|
287
|
+
/** A platform's write, as its platform author: idempotent per platform event for a post, found from its origin otherwise. */
|
|
288
|
+
syncImport: (body) => call("POST", "/sync/imports", { body }),
|
|
289
|
+
/** What sync did on a platform: a delivery, its files, a failure, or a thread's new target. */
|
|
290
|
+
syncReceipt: (body) => call("POST", "/sync/receipts", { body }),
|
|
291
|
+
/** A board thread's sync, by its key. */
|
|
292
|
+
syncThread: (thread) => call("GET", `/sync/threads/${path(thread)}`),
|
|
293
|
+
/** What platform IDs are on the board: a thread, a message, who an account is, a person's accounts. */
|
|
294
|
+
syncFind: (query, opts = {}) => call("GET", `/sync/find${queryString(query)}`, opts.signal ? { signal: opts.signal } : {}),
|
|
295
|
+
/** Messages' copies on platforms, by message and target. */
|
|
296
|
+
syncCopies: async (messages) => (await call("GET", `/sync/copies?${messages.map((id) => `message=${encodeURIComponent(id)}`).join("&")}`)).copies,
|
|
297
|
+
/** A person's roots since `since`. */
|
|
298
|
+
syncRoots: async (by, since) => (await call("GET", `/sync/roots${queryString({
|
|
299
|
+
by,
|
|
300
|
+
since: since instanceof Date ? since.getTime() : since
|
|
301
|
+
})}`)).roots,
|
|
302
|
+
/** Pushes platform events into your inbox for sync key `key`; each platform ID lands once. */
|
|
303
|
+
inboxPush: (key, events) => call("POST", `/inbox/${path(key)}`, { body: { events } }),
|
|
304
|
+
/** Your next inbox events for `key`, leased to you until you ack them, one platform thread at a time. */
|
|
305
|
+
inboxNext: async (key, opts = {}) => (await call("GET", `/inbox/${path(key)}/next${queryString(opts)}`)).items,
|
|
306
|
+
/** How an inbox event went: done, or an error to try it again. */
|
|
307
|
+
inboxAck: (key, id, body) => call("POST", `/inbox/${path(key)}/${path(id)}/ack`, { body }),
|
|
308
|
+
/**
|
|
309
|
+
* A one-time code for linking your account on sync entry `sync`, to post from that account where
|
|
310
|
+
* the board syncs, and `signIn` when the entry signs people in: open it (only you) and approve.
|
|
311
|
+
* Only people get one, each for themselves. `identity()` shows the link once it's made.
|
|
312
|
+
*/
|
|
313
|
+
syncCode: (sync) => call("POST", "/sync/codes", { body: { sync } }),
|
|
314
|
+
/** Links a platform account to whoever a code was issued to: one posted from it, or carried through its sign-in. */
|
|
315
|
+
syncAccount: (body) => call("POST", "/sync/accounts", { body }),
|
|
316
|
+
/** Unlinks a platform account: your own, or anyone's with `sync`. */
|
|
317
|
+
syncUnlink: (account) => call("POST", "/sync/accounts/unlink", { body: { account } }),
|
|
318
|
+
/** Live events. Reconnects from the last seen index whenever the stream closes. */
|
|
319
|
+
async *events(query = {}, opts = {}) {
|
|
320
|
+
let after = query.after;
|
|
321
|
+
let failures = 0;
|
|
322
|
+
while (!opts.signal?.aborted) {
|
|
323
|
+
let response;
|
|
324
|
+
try {
|
|
325
|
+
response = await raw("GET", `/events${queryString({
|
|
326
|
+
...filtersQuery(query),
|
|
327
|
+
after
|
|
328
|
+
})}`, {
|
|
329
|
+
headers: { accept: "text/event-stream" },
|
|
330
|
+
...opts.signal ? { signal: opts.signal } : {}
|
|
331
|
+
});
|
|
332
|
+
} catch (error) {
|
|
333
|
+
if (opts.signal?.aborted) return;
|
|
334
|
+
if (error instanceof BoardError && error.status >= 400 && error.status < 500) throw error;
|
|
335
|
+
await new Promise((r) => setTimeout(r, Math.min(3e4, 500 * 2 ** failures++)));
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
failures = 0;
|
|
339
|
+
try {
|
|
340
|
+
for await (const message of parseSse(response.body, opts.signal)) {
|
|
341
|
+
if (message.id !== void 0) after = message.id;
|
|
342
|
+
if (message.event === "board" && message.data) yield JSON.parse(message.data);
|
|
343
|
+
}
|
|
344
|
+
} catch (error) {
|
|
345
|
+
if (opts.signal?.aborted) return;
|
|
346
|
+
await new Promise((r) => setTimeout(r, Math.min(3e4, 500 * 2 ** failures++)));
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
},
|
|
350
|
+
/** The next matching event after `after` (default: now), or null on timeout. */
|
|
351
|
+
async wait(query = {}, opts = {}) {
|
|
352
|
+
const deadline = Date.now() + duration(opts.timeout ?? "30m");
|
|
353
|
+
let after = query.after ?? (await poll({
|
|
354
|
+
...query,
|
|
355
|
+
after: void 0,
|
|
356
|
+
wait: 0
|
|
357
|
+
}, opts.signal)).cursor;
|
|
358
|
+
while (Date.now() < deadline && !opts.signal?.aborted) {
|
|
359
|
+
const wait = Math.max(0, Math.min(25, Math.ceil((deadline - Date.now()) / 1e3)));
|
|
360
|
+
const page = await poll({
|
|
361
|
+
...query,
|
|
362
|
+
after,
|
|
363
|
+
wait
|
|
364
|
+
}, opts.signal);
|
|
365
|
+
if (page.events.length > 0) return page.events[0];
|
|
366
|
+
after = page.cursor;
|
|
367
|
+
}
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
async function toError(response) {
|
|
373
|
+
try {
|
|
374
|
+
const body = await response.json();
|
|
375
|
+
if (body.error?.message) return new BoardError(body.error.code ?? "error", body.error.message, response.status);
|
|
376
|
+
} catch {}
|
|
377
|
+
return new BoardError("http_error", `${response.status} ${response.statusText}`, response.status);
|
|
378
|
+
}
|
|
379
|
+
//#endregion
|
|
380
|
+
export { createBoardClient as n, duration as r, BoardError as t };
|