@vercel/board 0.29.0 → 0.30.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/{a2-HUbQJ0RD.mjs → a2-D_28Pdip.mjs} +190 -76
- package/dist/access-CQ39gqEV.mjs +226 -0
- package/dist/access.d.mts +1 -1
- package/dist/access.mjs +3 -3
- package/dist/{actors-DcJhFbwJ.mjs → actors-xPiamLWk.mjs} +2 -2
- package/dist/{auth-JO22iIwr.mjs → auth-BPinwfN0.mjs} +3 -226
- package/dist/backend-a2/blob-vercel.d.mts +1 -1
- package/dist/backend-a2/contract.d.mts +2 -2
- package/dist/backend-a2/contract.mjs +1 -1
- package/dist/backend-a2/scheduler-local.d.mts +1 -1
- package/dist/backend-a2/scheduler-local.mjs +1 -1
- package/dist/backend-a2/scheduler-qstash.d.mts +3 -3
- package/dist/backend-a2/scheduler-qstash.mjs +8 -8
- package/dist/backend-a2/scheduler-vercel.d.mts +3 -3
- package/dist/backend-a2/scheduler-vercel.mjs +6 -6
- package/dist/backend-a2/store-memory.d.mts +2 -2
- package/dist/backend-a2/store-memory.mjs +3 -3
- package/dist/backend-a2/store-postgres.d.mts +1 -1
- package/dist/backend-a2/store-postgres.mjs +34 -15
- package/dist/backend-a2/store-redis-http.d.mts +1 -1
- package/dist/backend-a2/store-redis-http.mjs +6 -6
- package/dist/backend-a2/store-redis.d.mts +2 -2
- package/dist/backend-a2/store-redis.mjs +5 -5
- package/dist/backend-a2/store-sqlite.d.mts +1 -1
- package/dist/backend-a2/store-sqlite.mjs +3 -3
- package/dist/backend-a2.d.mts +17 -7
- package/dist/backend-a2.mjs +2 -2
- package/dist/{backend-http-Bmp0IgD3.mjs → backend-http-16SbvnAK.mjs} +2 -2
- package/dist/{backend-http-CqfKhrnn.d.mts → backend-http-3PoBYQoo.d.mts} +13 -3
- package/dist/{backend-http-CzIJMm58.mjs → backend-http-BPaNYLWj.mjs} +2 -3
- package/dist/backend-http.d.mts +1 -1
- package/dist/backend-http.mjs +1 -1
- package/dist/backend-http.node.d.mts +1 -1
- package/dist/backend-http.node.mjs +1 -1
- package/dist/{backend-CPC_vyOo.d.mts → backend-n3hajfrD.d.mts} +11 -5
- package/dist/{backend-vercel-BNauAs7w.mjs → backend-vercel-C4Kee2-K.mjs} +203 -31
- package/dist/{backend-vercel-DQc_TaHp.mjs → backend-vercel-Kd8nvW5A.mjs} +3 -3
- package/dist/{backend-vercel-4tvcPKqX.d.mts → backend-vercel-qznuDxnN.d.mts} +16 -5
- package/dist/backend-vercel.d.mts +1 -1
- package/dist/backend-vercel.mjs +1 -1
- package/dist/backend-vercel.node.d.mts +1 -1
- package/dist/backend-vercel.node.mjs +1 -1
- package/dist/{blob-DmiQjrYy.d.mts → blob-CqILvdmr.d.mts} +2 -2
- package/dist/{blob-DdbpJqN1.mjs → blob-DQK5prXT.mjs} +1 -1
- package/dist/{chat-provider-BWHbChcQ.d.mts → chat-provider-D6jVT4wS.d.mts} +3 -3
- package/dist/cli-commands.d.mts +1 -1
- package/dist/cli-commands.mjs +1 -1
- package/dist/cli-embedded.d.mts +2 -2
- package/dist/cli-embedded.mjs +1 -1
- package/dist/cli.mjs +4 -4
- package/dist/{client-BqAjk0Q4.d.mts → client-CjAatpV4.d.mts} +60 -10
- package/dist/{commands-C3R1AWgm.mjs → commands-B5DHjZKK.mjs} +50 -9
- package/dist/{commands-D6gvrOh3.d.mts → commands-BtH69yqt.d.mts} +17 -0
- package/dist/{conformance-DlnfuzEB.mjs → conformance-C7fwIvmN.mjs} +258 -18
- package/dist/conformance.d.mts +1 -1
- package/dist/conformance.mjs +1 -1
- package/dist/{engine-DQxXk7dX.mjs → engine-DkdkXQEy.mjs} +2 -2
- package/dist/{errors-DDktVeUa.d.mts → errors-87TVtXeb.d.mts} +1 -1
- package/dist/{client-D0Px7KzF.mjs → errors-C_5rB8Iz.mjs} +104 -4
- package/dist/{exports-BG-0Wyn-.mjs → exports-CG0P8Qi4.mjs} +54 -28
- package/dist/exports-CrGA5_yN.d.mts +5 -0
- package/dist/{folds-ZkHM4M-v.d.mts → folds-8wRRXjAt.d.mts} +2 -0
- package/dist/{folds-BDMSxBUf.mjs → folds-DGYkMW19.mjs} +1 -0
- package/dist/{index-D3VvNfh_.d.mts → index-BygAXP7t.d.mts} +2 -2
- package/dist/index.d.mts +6 -6
- package/dist/{index.default-rB8sJ9RX.mjs → index.default-DhkgpfR2.mjs} +1 -1
- package/dist/index.mjs +5 -6
- package/dist/index.node.d.mts +6 -6
- package/dist/index.node.mjs +3 -4
- package/dist/inspect/app.js +29 -29
- package/dist/{internal-CvsrprnU-CRY89I-b.mjs → internal-CvsrprnU-zZMpDiRY.mjs} +1 -1
- package/dist/{local-DhkrAqPP.mjs → local-C76Fy5ad.mjs} +1 -1
- package/dist/{main-CVBqe3VO.mjs → main-D2s-QBOK.mjs} +70 -23
- package/dist/{middleware-CXjEeef4.mjs → middleware-DlJQYiwV.mjs} +1 -1
- package/dist/{node-Zd_C-gxP.mjs → node-D8Rtg1su.mjs} +1 -1
- package/dist/{platform-B4TnJtWu-CwcTyKg2.mjs → platform-B4TnJtWu-Do3agKr5.mjs} +1 -1
- package/dist/{project-CBXlLftU.mjs → project-C_2xs57L.mjs} +1 -1
- package/dist/{provider-DmSTp9lI.d.mts → provider-Bm46LAdp.d.mts} +2 -2
- package/dist/{redis-tables-BJQVUspu.mjs → redis-tables-BWJZHUGg.mjs} +40 -24
- package/dist/{reducer-CjmvZSAb-D_OSISmj.mjs → reducer-Bswr11ar-P324o9vn.mjs} +2 -2
- package/dist/{reducer-CYPHsSmd-BQIfozGi.d.mts → reducer-CYPHsSmd-BXjGiptm.d.mts} +1 -1
- package/dist/{reducers-DJzVpXwM.mjs → reducers-8jqLewgw.mjs} +3 -3
- package/dist/{retryable-lazy-DZWmHpii-Bye-W8q7.mjs → retryable-lazy-DZWmHpii-DpTc5PIP.mjs} +1 -1
- package/dist/{scheduler-task-X1SToDjP-BVoWuInI.mjs → scheduler-task-X1SToDjP-D2Il2rln.mjs} +1 -1
- package/dist/{serve-G9RuaDSr.mjs → serve-lPoBDEds.mjs} +7 -7
- package/dist/{server-qgez0Xny-D5mLfqFQ.d.mts → server-B9gu0qi1-DI1WvcVK.d.mts} +10 -4
- package/dist/{server-BixV_byP.mjs → server-D82a4uMe.mjs} +4 -4
- package/dist/{server-DtFrWxi3-Boa6gdWO.mjs → server-DX7mM6nm-D7N_7bug.mjs} +32 -23
- package/dist/{serving-B75Y4BF-.mjs → serving-DKny38LK.mjs} +1 -1
- package/dist/{session-clear-wHd5GCpZ-BnKSxReK.mjs → session-clear-XJYpmUpg-2q8l_FJR.mjs} +14 -2
- package/dist/{sql-D4Z_efJX.mjs → sql-Bj0fk6dn.mjs} +1 -1
- package/dist/{sqlite-db-Bz2JkfCU.mjs → sqlite-db-Dc8G78XV.mjs} +1 -1
- package/dist/{store-D0MhBvg6-4Z2L0Kqd.d.mts → store-DEzcHZOA-B-5vhxef.d.mts} +12 -2
- package/dist/store-N8PXxDAS-DWWgv-Vz.mjs +5 -0
- package/dist/{store-codec-DwrWlgQx-CYxGW0aD.mjs → store-codec-pAG2oPgQ-SqDXlPBR.mjs} +4 -4
- package/dist/store-memory-4IBl0Ztu.mjs +2 -0
- package/dist/{store-memory-Bas9Uq5C.mjs → store-memory-B0PkVSuQ.mjs} +14 -7
- package/dist/{store-polling-BZt2Rf3D-BiEM6_HB.mjs → store-polling-BZt2Rf3D-Div-bJ_D.mjs} +2 -2
- package/dist/{store-presence-polling-JuAy2Ck2-CYpuK75X.mjs → store-presence-polling-JuAy2Ck2-Dh-g_KYM.mjs} +3 -3
- package/dist/{store-sqlite-CxxPCcCT.mjs → store-sqlite-D85t3tE7.mjs} +14 -7
- package/dist/store-sqlite-iBTYYKBj.mjs +2 -0
- package/dist/{stream-buffer-DrTyiIuo-BKy-vI14.mjs → stream-buffer-DrTyiIuo-Be10yOBR.mjs} +2 -2
- package/dist/sync-github.d.mts +2 -2
- package/dist/sync-kapso.d.mts +2 -2
- package/dist/sync-linear.d.mts +3 -3
- package/dist/sync-linear.mjs +4 -2
- package/dist/sync-slack.d.mts +3 -3
- package/dist/sync.chat.d.mts +1 -1
- package/dist/sync.d.mts +2 -2
- package/dist/{tokens-CC3izSNr.mjs → tokens-C5_yGRlL.mjs} +2 -2
- package/dist/{types-No0WdhE5.d.mts → types-shZmGZ-q.d.mts} +2 -2
- package/dist/{watchers-hash-BDhLVkV5.mjs → watchers-hash-B_5b0-ZG.mjs} +46 -17
- package/docs/01-board/04-messages.md +93 -0
- package/docs/01-board/08-middleware.md +5 -2
- package/docs/04-reference/01-cli.md +23 -10
- package/docs/04-reference/02-board.md +20 -5
- package/docs/04-reference/03-http.md +17 -6
- package/docs/04-reference/05-conformance.md +14 -3
- package/package.json +2 -2
- package/dist/errors-CusN4x2l.mjs +0 -88
- package/dist/exports-mhAYiU4i.d.mts +0 -5
- package/dist/store-N8PXxDAS-BWBgzBHp.mjs +0 -5
- package/dist/store-memory-ZLiZbH3R.mjs +0 -2
- package/dist/store-sqlite-BmwL3pUR.mjs +0 -2
|
@@ -1,31 +1,31 @@
|
|
|
1
1
|
import { r as IMPORT_BYTES_MAX, t as ATTACHMENT_BYTES_MAX } from "./files-SniSmeEr.mjs";
|
|
2
2
|
import { _ as RemoteWatcherId, b as actorKind, x as platformAvatar } from "./schemas-DBmUO6pr.mjs";
|
|
3
3
|
import "./external-D3p5T57R.mjs";
|
|
4
|
-
import { a as METHODS, d as VERSION, f as SYNC, n as compileWatcher, r as hasCriteria, t as canonicalWatchers } from "./watchers-hash-
|
|
5
|
-
import {
|
|
4
|
+
import { a as METHODS, d as VERSION, f as SYNC, n as compileWatcher, r as hasCriteria, t as canonicalWatchers } from "./watchers-hash-B_5b0-ZG.mjs";
|
|
5
|
+
import { a as forbidden, c as toErrorResponse, f as duration, i as duplicateId, r as badRequest, s as notFound, t as HttpError, u as BoardError } from "./errors-C_5rB8Iz.mjs";
|
|
6
6
|
import { i as isKeyIdOf, r as derivedId, t as waitUntil } from "./wait-until-Dw4XrIDQ.mjs";
|
|
7
|
-
import { a as forbidden, c as toErrorResponse, i as duplicateId, r as badRequest, s as notFound, t as HttpError } from "./errors-CusN4x2l.mjs";
|
|
8
7
|
import { t as own } from "./own-OwgPWdmh.mjs";
|
|
9
8
|
import { n as replaceMentions } from "./mentions-CB6ShY4R.mjs";
|
|
10
|
-
import {
|
|
11
|
-
import { a as resolveMentions, n as actorSeen } from "./actors-
|
|
12
|
-
import {
|
|
9
|
+
import { n as authorize, t as FULL_ACCESS } from "./access-CQ39gqEV.mjs";
|
|
10
|
+
import { a as resolveMentions, n as actorSeen } from "./actors-xPiamLWk.mjs";
|
|
11
|
+
import { i as authenticates } from "./auth-BPinwfN0.mjs";
|
|
12
|
+
import { n as MIDDLEWARE_ACTOR, t as DroppedError } from "./middleware-DlJQYiwV.mjs";
|
|
13
13
|
import { n as syncTarget, r as withEmailMentions } from "./targets-Cgjs29T0.mjs";
|
|
14
|
-
import { t as A2Error } from "./session-clear-
|
|
15
|
-
import { a as reducer } from "./reducer-
|
|
16
|
-
import { _ as BOARD_SESSION, a as IDENTITY_SESSION, c as SCHEDULES_SESSION, d as inboxContract, f as inboxSession, g as watchSession, h as watchContract, l as boardContract, m as schedulesContract, n as channelsReducer, o as INBOXES_SESSION, p as registryContract, r as identityCodesReducer, s as REGISTRY_SESSION, t as actorsReducer, u as identityContract } from "./reducers-
|
|
17
|
-
import { n as createServer, t as consumer } from "./server-
|
|
18
|
-
import { a as linkedTo, i as iso, n as foldMessages, o as personOf, t as foldActors } from "./folds-
|
|
19
|
-
import { c as readUpload, i as finishUpload, l as startUpload, n as attachmentNotFound, t as attachmentBody, u as writeUpload } from "./blob-
|
|
20
|
-
import { t as concernedMessageId } from "./project-
|
|
21
|
-
import { t as TABLES_CONSUMER } from "./sql-
|
|
14
|
+
import { t as A2Error } from "./session-clear-XJYpmUpg-2q8l_FJR.mjs";
|
|
15
|
+
import { a as reducer } from "./reducer-Bswr11ar-P324o9vn.mjs";
|
|
16
|
+
import { _ as BOARD_SESSION, a as IDENTITY_SESSION, c as SCHEDULES_SESSION, d as inboxContract, f as inboxSession, g as watchSession, h as watchContract, l as boardContract, m as schedulesContract, n as channelsReducer, o as INBOXES_SESSION, p as registryContract, r as identityCodesReducer, s as REGISTRY_SESSION, t as actorsReducer, u as identityContract } from "./reducers-8jqLewgw.mjs";
|
|
17
|
+
import { n as createServer, t as consumer } from "./server-DX7mM6nm-D7N_7bug.mjs";
|
|
18
|
+
import { a as linkedTo, i as iso, n as foldMessages, o as personOf, t as foldActors } from "./folds-DGYkMW19.mjs";
|
|
19
|
+
import { c as readUpload, i as finishUpload, l as startUpload, n as attachmentNotFound, t as attachmentBody, u as writeUpload } from "./blob-DQK5prXT.mjs";
|
|
20
|
+
import { t as concernedMessageId } from "./project-C_2xs57L.mjs";
|
|
21
|
+
import { t as TABLES_CONSUMER } from "./sql-Bj0fk6dn.mjs";
|
|
22
22
|
import { t as afterRetry } from "./attempts-HXcskN1w.mjs";
|
|
23
23
|
import { createHash, randomBytes } from "node:crypto";
|
|
24
24
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
25
25
|
import { lookup } from "node:dns";
|
|
26
26
|
import { request } from "node:https";
|
|
27
27
|
import { BlockList, isIP } from "node:net";
|
|
28
|
-
//#region ../../node_modules/.pnpm/experimental-a2@0.
|
|
28
|
+
//#region ../../node_modules/.pnpm/experimental-a2@0.24.0_@upstash+qstash@2.12.0_@vercel+queue@0.7.0_ai@7.0.130_zod@4.6.5__b1e2c243a963f2620a5d202817417c26/node_modules/experimental-a2/dist/idempotent-id-BRJVeylj.js
|
|
29
29
|
async function idempotentId(...parts) {
|
|
30
30
|
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify(parts)));
|
|
31
31
|
const bytes = new Uint8Array(digest).slice(0, 16);
|
|
@@ -125,11 +125,13 @@ function viewedBy(view, reader) {
|
|
|
125
125
|
}
|
|
126
126
|
/** A message's or a cursor's ID as clients see it. On this board it's the log index, in decimal. */
|
|
127
127
|
const ref = (index) => String(index);
|
|
128
|
+
/** A schedule's ID as clients see it: its index in session `schedules`, as `s12`, so it reads apart from message `12`. */
|
|
129
|
+
const scheduleRef = (index) => `s${index}`;
|
|
128
130
|
/** The log index a ref names on this board, or null for a string that names nothing here. */
|
|
129
131
|
const indexOf = (value) => /^(0|[1-9]\d{0,15})$/.test(value) ? Number(value) : null;
|
|
130
132
|
/** A message as `reader` sees it. Every view names its reader, so none shows a link's snapshot by default. */
|
|
131
133
|
function view(message, actors, reader) {
|
|
132
|
-
const { blind, version, schedule, ...shown } = message;
|
|
134
|
+
const { blind, version, revision, schedule, ...shown } = message;
|
|
133
135
|
return {
|
|
134
136
|
...shown,
|
|
135
137
|
...blind ? { blind } : {},
|
|
@@ -137,8 +139,9 @@ function view(message, actors, reader) {
|
|
|
137
139
|
re: message.re === null ? null : ref(message.re),
|
|
138
140
|
replyTo: message.replyTo === null ? null : ref(message.replyTo),
|
|
139
141
|
version: version ?? message.id,
|
|
142
|
+
revision: revision ?? message.id,
|
|
140
143
|
...schedule ? { schedule: {
|
|
141
|
-
id:
|
|
144
|
+
id: scheduleRef(schedule.id),
|
|
142
145
|
n: schedule.n
|
|
143
146
|
} } : {},
|
|
144
147
|
links: readableLinks(message.links, reader),
|
|
@@ -325,7 +328,7 @@ function payloadView(payload, links) {
|
|
|
325
328
|
]) if (typeof shown[key] === "number") shown[key] = ref(shown[key]);
|
|
326
329
|
const schedule = shown.schedule;
|
|
327
330
|
if (schedule) shown.schedule = {
|
|
328
|
-
id:
|
|
331
|
+
id: scheduleRef(schedule.id),
|
|
329
332
|
n: schedule.n
|
|
330
333
|
};
|
|
331
334
|
if (links) shown.links = links;
|
|
@@ -750,19 +753,20 @@ async function writePost(ctx, input) {
|
|
|
750
753
|
if (input.sync?.length) throw badRequest("sync attaches whole threads: use `board sync <id> <target>`");
|
|
751
754
|
} else {
|
|
752
755
|
if (!channel) throw badRequest("a post needs a channel");
|
|
756
|
+
if (input.after !== void 0) throw badRequest("`after` is for replies", "invalid_after");
|
|
753
757
|
thread = id;
|
|
754
758
|
}
|
|
755
759
|
spend(input.proofs.post, "post", { channel });
|
|
756
760
|
if (hasLabels(input.labels)) spend(input.proofs.label, "label", { channel });
|
|
757
761
|
if (input.sync?.length) spend(input.proofs.sync, "sync", { channel });
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
}
|
|
762
|
+
const replayed = await replayOf(ctx, input, {
|
|
763
|
+
channel,
|
|
764
|
+
re,
|
|
765
|
+
replyTo
|
|
766
|
+
}, sent);
|
|
767
|
+
if (replayed) return replayed;
|
|
768
|
+
if (input.after !== void 0) {
|
|
769
|
+
if ((await ctx.reads.message(input.after))?.thread !== thread) throw badRequest(`after names no message in #${re}'s thread`, "invalid_after");
|
|
766
770
|
}
|
|
767
771
|
return commitPost(ctx, input, {
|
|
768
772
|
channel,
|
|
@@ -773,6 +777,16 @@ async function writePost(ctx, input) {
|
|
|
773
777
|
sent
|
|
774
778
|
});
|
|
775
779
|
}
|
|
780
|
+
/** The post a caller's own ID already landed, as its retry gets it back: null when there's none. */
|
|
781
|
+
async function replayOf(ctx, input, place, sent) {
|
|
782
|
+
if (input.id === void 0) return null;
|
|
783
|
+
const landed = await ctx.reads.byEventId(input.id);
|
|
784
|
+
if (!landed) return null;
|
|
785
|
+
if (landed.channel !== place.channel || landed.re !== place.re || landed.replyTo !== place.replyTo) throw duplicateId();
|
|
786
|
+
const shown = await landedFor(ctx, input.identity, landed);
|
|
787
|
+
if (!shown) throw duplicateId();
|
|
788
|
+
return shownTo(shown, input.identity.principal.access, sent);
|
|
789
|
+
}
|
|
776
790
|
/**
|
|
777
791
|
* A post that landed, as its author may have it back: only the same author's, and one in a
|
|
778
792
|
* channel it can't read only if it posted without reading too. A caller that can't read gets back
|
|
@@ -833,28 +847,45 @@ async function commitPost(ctx, input, placed) {
|
|
|
833
847
|
by: identity.author
|
|
834
848
|
}
|
|
835
849
|
})));
|
|
836
|
-
const
|
|
837
|
-
|
|
838
|
-
...
|
|
839
|
-
|
|
850
|
+
const events = [
|
|
851
|
+
...seen ? [seen] : [],
|
|
852
|
+
...named,
|
|
853
|
+
{
|
|
854
|
+
type: "message.posted",
|
|
855
|
+
...id ? { id } : {},
|
|
856
|
+
payload: {
|
|
857
|
+
channel,
|
|
858
|
+
text,
|
|
859
|
+
re,
|
|
860
|
+
replyTo,
|
|
861
|
+
thread,
|
|
862
|
+
labels: input.labels ?? {},
|
|
863
|
+
noSync: input.noSync ?? false,
|
|
864
|
+
mentions,
|
|
865
|
+
by: identity.author,
|
|
866
|
+
origin: input.origin ?? null,
|
|
867
|
+
from: input.from ?? null,
|
|
868
|
+
...input.attachments?.length ? { attachments: input.attachments } : {},
|
|
869
|
+
...input.links?.length ? { links: input.links } : {},
|
|
870
|
+
...input.schedule ? { schedule: input.schedule } : {},
|
|
871
|
+
...identity.principal.access.readable(channel) ? {} : { blind: true },
|
|
872
|
+
...input.importedBy !== void 0 ? { importedBy: input.importedBy } : {}
|
|
873
|
+
}
|
|
874
|
+
},
|
|
875
|
+
...synced
|
|
876
|
+
];
|
|
877
|
+
let rows;
|
|
878
|
+
try {
|
|
879
|
+
rows = input.after === void 0 || re === null ? await ctx.session.append(...events) : await appendWhile(ctx, nothingAfter(ctx, re, input.after), events);
|
|
880
|
+
} catch (error) {
|
|
881
|
+
const replayed = error instanceof HttpError && error.code === "newer_message" ? await replayOf(ctx, input, {
|
|
840
882
|
channel,
|
|
841
|
-
text,
|
|
842
883
|
re,
|
|
843
|
-
replyTo
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
by: identity.author,
|
|
849
|
-
origin: input.origin ?? null,
|
|
850
|
-
from: input.from ?? null,
|
|
851
|
-
...input.attachments?.length ? { attachments: input.attachments } : {},
|
|
852
|
-
...input.links?.length ? { links: input.links } : {},
|
|
853
|
-
...input.schedule ? { schedule: input.schedule } : {},
|
|
854
|
-
...identity.principal.access.readable(channel) ? {} : { blind: true },
|
|
855
|
-
...input.importedBy !== void 0 ? { importedBy: input.importedBy } : {}
|
|
856
|
-
}
|
|
857
|
-
}, ...synced);
|
|
884
|
+
replyTo
|
|
885
|
+
}, sent) : null;
|
|
886
|
+
if (replayed) return replayed;
|
|
887
|
+
throw error;
|
|
888
|
+
}
|
|
858
889
|
const posted = rows.find((row) => row.type === "message.posted");
|
|
859
890
|
await untilApplied(ctx, rows);
|
|
860
891
|
const message = foldMessages({
|
|
@@ -923,6 +954,36 @@ async function untilApplied(ctx, rows) {
|
|
|
923
954
|
}
|
|
924
955
|
/** How long a write waits for the tables before it answers anyway. */
|
|
925
956
|
const LANDED_WAIT_MS = 1e4;
|
|
957
|
+
/**
|
|
958
|
+
* Appends `events` only while the board, as `holds` reads it, still allows them: `holds` checks
|
|
959
|
+
* the tables, throws what refuses the write, and returns the index its reads reflect. The append
|
|
960
|
+
* holds only while that index is still the log's last, so anything that lands in between,
|
|
961
|
+
* anywhere on the board, moves it, and it looks again from there.
|
|
962
|
+
*/
|
|
963
|
+
async function appendWhile(ctx, holds, events) {
|
|
964
|
+
for (let attempt = 1;; attempt++) {
|
|
965
|
+
await ctx.reads.ensure();
|
|
966
|
+
const position = await holds();
|
|
967
|
+
try {
|
|
968
|
+
return await appendIfLast(ctx.session, position, events);
|
|
969
|
+
} catch (error) {
|
|
970
|
+
if (!headMoved(error)) throw error;
|
|
971
|
+
if (attempt === CONDITIONAL_TRIES) throw new HttpError(503, "unavailable", "the board kept changing while it checked: try again");
|
|
972
|
+
await new Promise((resolve) => setTimeout(resolve, Math.random() * 5 * attempt));
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
/** How many times a conditional write looks again, as the board moves on, before it gives up. */
|
|
977
|
+
const CONDITIONAL_TRIES = 10;
|
|
978
|
+
/** A reply's `after`: nothing came after it in the thread, as of the index the read reflects. */
|
|
979
|
+
const nothingAfter = (ctx, root, after) => async () => {
|
|
980
|
+
const { messages: [newer], position } = await ctx.reads.tables.messages({ re: root }, {
|
|
981
|
+
after,
|
|
982
|
+
limit: 1
|
|
983
|
+
});
|
|
984
|
+
if (newer) throw new HttpError(409, "newer_message", `#${newer.id} came after #${after}`);
|
|
985
|
+
return position;
|
|
986
|
+
};
|
|
926
987
|
/** The message a by-ID write targets, as the board looked it up, once its proof is spent on the message's channel. */
|
|
927
988
|
function spent(authorized, permission) {
|
|
928
989
|
spend(authorized.proof, permission, authorized.message);
|
|
@@ -931,7 +992,10 @@ function spent(authorized, permission) {
|
|
|
931
992
|
async function writeEdit(ctx, input) {
|
|
932
993
|
const message = spent(input.authorized, "post");
|
|
933
994
|
if (input.asAuthor !== false && message.by.principal !== input.identity.principal.id) throw forbidden("only the author can edit a message");
|
|
934
|
-
if (message.deleted)
|
|
995
|
+
if (message.deleted) {
|
|
996
|
+
if (input.revision !== void 0 && input.revision !== (message.revision ?? message.id)) throw new HttpError(409, "revision_conflict", `#${message.id} was deleted since revision ${input.revision}`);
|
|
997
|
+
throw badRequest("message was deleted", "message_deleted");
|
|
998
|
+
}
|
|
935
999
|
return commitEdit(ctx, input, message);
|
|
936
1000
|
}
|
|
937
1001
|
async function commitEdit(ctx, input, message) {
|
|
@@ -940,17 +1004,22 @@ async function commitEdit(ctx, input, message) {
|
|
|
940
1004
|
const { text, found } = await withEmailMentions(input.text);
|
|
941
1005
|
const named = found.map((actor) => actorSeen(actor, actors)).filter((event) => event !== null);
|
|
942
1006
|
const { mentions, unknown } = resolveMentions(text, knownWith(actors, found), seen ? input.identity.actor : void 0, input.from != null, input.identity.principal.access.readsAll);
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
1007
|
+
const events = [
|
|
1008
|
+
...seen ? [seen] : [],
|
|
1009
|
+
...named,
|
|
1010
|
+
{
|
|
1011
|
+
type: "message.edited",
|
|
1012
|
+
payload: {
|
|
1013
|
+
id: message.id,
|
|
1014
|
+
thread: message.thread,
|
|
1015
|
+
text,
|
|
1016
|
+
mentions,
|
|
1017
|
+
by: input.identity.author,
|
|
1018
|
+
from: input.from ?? null
|
|
1019
|
+
}
|
|
952
1020
|
}
|
|
953
|
-
|
|
1021
|
+
];
|
|
1022
|
+
await untilApplied(ctx, input.revision === void 0 ? await ctx.session.append(...events) : await appendWhile(ctx, revisionIs(ctx, message.id, input.revision), events));
|
|
954
1023
|
return unknown.length && input.identity.principal.access.readable(message.channel) ? { unknownMentions: unknown } : {};
|
|
955
1024
|
}
|
|
956
1025
|
/** A message's links, replaced: a platform edit or unfurl changed what it links to. */
|
|
@@ -971,8 +1040,11 @@ async function writeLinks(ctx, input) {
|
|
|
971
1040
|
async function writeDelete(ctx, input) {
|
|
972
1041
|
const message = spent(input.authorized, "post");
|
|
973
1042
|
if (input.asAuthor !== false && message.by.principal !== input.identity.principal.id) throw forbidden("only the author can delete a message");
|
|
974
|
-
if (message.deleted)
|
|
975
|
-
|
|
1043
|
+
if (message.deleted) {
|
|
1044
|
+
if (input.revision !== void 0 && input.revision !== (message.revision ?? message.id)) throw new HttpError(409, "revision_conflict", `#${message.id} was deleted since revision ${input.revision}`);
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
1047
|
+
const deleted = [{
|
|
976
1048
|
type: "message.deleted",
|
|
977
1049
|
payload: {
|
|
978
1050
|
id: message.id,
|
|
@@ -980,8 +1052,17 @@ async function writeDelete(ctx, input) {
|
|
|
980
1052
|
by: input.identity.author,
|
|
981
1053
|
from: input.from ?? null
|
|
982
1054
|
}
|
|
983
|
-
}
|
|
984
|
-
|
|
1055
|
+
}];
|
|
1056
|
+
await untilApplied(ctx, input.revision === void 0 ? await ctx.session.append(...deleted) : await appendWhile(ctx, revisionIs(ctx, message.id, input.revision), deleted));
|
|
1057
|
+
}
|
|
1058
|
+
/** An edit's or a delete's `revision`: no edit or delete since, as of the index the read reflects. */
|
|
1059
|
+
const revisionIs = (ctx, id, revision) => async () => {
|
|
1060
|
+
const position = await ctx.reads.tables.position();
|
|
1061
|
+
const message = (await ctx.reads.tables.messagesAt([id], position)).get(id);
|
|
1062
|
+
const now = message ? message.revision ?? message.id : null;
|
|
1063
|
+
if (now !== revision) throw new HttpError(409, "revision_conflict", message?.deleted ? `#${id} was deleted since revision ${revision}` : `#${id} was edited since revision ${revision}: it's at ${now}`);
|
|
1064
|
+
return position;
|
|
1065
|
+
};
|
|
985
1066
|
async function writeReaction(ctx, input) {
|
|
986
1067
|
const message = spent(input.authorized, "react");
|
|
987
1068
|
if (message.deleted) throw badRequest("message was deleted", "message_deleted");
|
|
@@ -1003,7 +1084,7 @@ async function writeLabels(ctx, input) {
|
|
|
1003
1084
|
if (message.deleted) throw badRequest("message was deleted", "message_deleted");
|
|
1004
1085
|
const { state: actors } = await ctx.session.state(actorsReducer);
|
|
1005
1086
|
const seen = seenFor(input.identity, actors);
|
|
1006
|
-
|
|
1087
|
+
const events = [...seen ? [seen] : [], {
|
|
1007
1088
|
type: "labels.changed",
|
|
1008
1089
|
payload: {
|
|
1009
1090
|
id: message.id,
|
|
@@ -1012,8 +1093,21 @@ async function writeLabels(ctx, input) {
|
|
|
1012
1093
|
unset: input.unset ?? [],
|
|
1013
1094
|
by: input.identity.author
|
|
1014
1095
|
}
|
|
1015
|
-
}
|
|
1016
|
-
|
|
1096
|
+
}];
|
|
1097
|
+
await untilApplied(ctx, input.if !== void 0 && Object.keys(input.if).length > 0 ? await appendWhile(ctx, labelsAre(ctx, message.id, input.if), events) : await ctx.session.append(...events));
|
|
1098
|
+
}
|
|
1099
|
+
/** A label write's `if`: each label is as it says, as of the index the read reflects. */
|
|
1100
|
+
const labelsAre = (ctx, id, expected) => async () => {
|
|
1101
|
+
const position = await ctx.reads.tables.position();
|
|
1102
|
+
const labels = (await ctx.reads.tables.messagesAt([id], position)).get(id)?.labels ?? {};
|
|
1103
|
+
for (const key of Object.keys(expected).sort()) {
|
|
1104
|
+
const now = own(labels, key);
|
|
1105
|
+
if (now === expected[key] || now === void 0 && expected[key] === null) continue;
|
|
1106
|
+
const wanted = expected[key];
|
|
1107
|
+
throw new HttpError(409, "label_conflict", wanted === null ? `${key} is set` : now === void 0 ? `${key} isn't set` : `${key} isn't ${wanted}`);
|
|
1108
|
+
}
|
|
1109
|
+
return position;
|
|
1110
|
+
};
|
|
1017
1111
|
/**
|
|
1018
1112
|
* Syncs a message's thread with a target, resolved by the sync that delivers it. Attaching twice
|
|
1019
1113
|
* is a no-op; a platform thread another board thread is synced with is a 409.
|
|
@@ -1037,6 +1131,10 @@ async function writeAttach(ctx, input) {
|
|
|
1037
1131
|
}));
|
|
1038
1132
|
return { attached: true };
|
|
1039
1133
|
}
|
|
1134
|
+
/** The batch, only while `index` is still the session's last: A2's conditional append. */
|
|
1135
|
+
const appendIfLast = (session, index, events) => session.append.ifLastIndex(index, ...events);
|
|
1136
|
+
/** What A2 throws when the session's last index isn't the one a conditional append named. */
|
|
1137
|
+
const headMoved = (error) => error?.code === "LAST_INDEX_MISMATCH";
|
|
1040
1138
|
//#endregion
|
|
1041
1139
|
//#region src/server/schedules.ts
|
|
1042
1140
|
/** Vercel Queues delays at most 6 days: a send further out than this goes in hops. */
|
|
@@ -1137,7 +1235,7 @@ const createdBy = (schedule, principal) => schedule.as.principal.id === principa
|
|
|
1137
1235
|
const scheduleView = (schedule, actors) => {
|
|
1138
1236
|
const author = actors.byId[schedule.as.actor.id] ?? schedule.as.actor;
|
|
1139
1237
|
return {
|
|
1140
|
-
id:
|
|
1238
|
+
id: scheduleRef(schedule.id),
|
|
1141
1239
|
channel: schedule.channel,
|
|
1142
1240
|
re: schedule.re === null ? null : ref(schedule.re),
|
|
1143
1241
|
text: schedule.text,
|
|
@@ -2349,9 +2447,17 @@ function messageAt(value) {
|
|
|
2349
2447
|
if (index === null || index === 0) throw notFound(`message not found: #${value}`, "message_not_found");
|
|
2350
2448
|
return index;
|
|
2351
2449
|
}
|
|
2352
|
-
/**
|
|
2450
|
+
/**
|
|
2451
|
+
* A reply's `after`, as the index of a message. One that isn't one is 0, which names no message:
|
|
2452
|
+
* the post refuses it, after a retry that landed gets its reply back.
|
|
2453
|
+
*/
|
|
2454
|
+
const afterAt = (value) => value === void 0 ? void 0 : indexOf(value) ?? 0;
|
|
2455
|
+
/**
|
|
2456
|
+
* A schedule's ID, `s12`, as its index in session `schedules`. `12` from a client that strips the
|
|
2457
|
+
* `s`, and `ss12` from an older CLI that adds one to what this board hands out, name it too.
|
|
2458
|
+
*/
|
|
2353
2459
|
function scheduleAt(value) {
|
|
2354
|
-
const bare = value.replace(/^s/, "");
|
|
2460
|
+
const bare = value.replace(/^s{1,2}/, "");
|
|
2355
2461
|
const index = indexOf(bare);
|
|
2356
2462
|
if (index === null || index === 0) throw notFound(`schedule not found: s${bare}`, "schedule_not_found");
|
|
2357
2463
|
return index;
|
|
@@ -2722,6 +2828,7 @@ function build(options, binding) {
|
|
|
2722
2828
|
noSync: body.noSync,
|
|
2723
2829
|
id: body.id,
|
|
2724
2830
|
sync: body.sync,
|
|
2831
|
+
after: afterAt(body.after),
|
|
2725
2832
|
...schedule && !fromRequest(caller) ? {
|
|
2726
2833
|
schedule,
|
|
2727
2834
|
lenientMentions: true
|
|
@@ -2880,25 +2987,27 @@ function build(options, binding) {
|
|
|
2880
2987
|
async post(caller, parsed) {
|
|
2881
2988
|
return postNow(caller, await identityOf(caller), parsed);
|
|
2882
2989
|
},
|
|
2883
|
-
async edit(caller, { id, text }) {
|
|
2990
|
+
async edit(caller, { id, text, revision }) {
|
|
2884
2991
|
const identity = await identityOf(caller);
|
|
2885
2992
|
const authorized = await authorizeOwn(writes, identity.principal, messageAt(id), "edit");
|
|
2886
2993
|
const { unknownMentions } = await writeEdit(writes, {
|
|
2887
2994
|
identity,
|
|
2888
2995
|
authorized,
|
|
2889
|
-
text
|
|
2996
|
+
text,
|
|
2997
|
+
revision
|
|
2890
2998
|
});
|
|
2891
2999
|
return {
|
|
2892
3000
|
ok: true,
|
|
2893
3001
|
...unknownMentions ? { unknownMentions } : {}
|
|
2894
3002
|
};
|
|
2895
3003
|
},
|
|
2896
|
-
async delete(caller, { id }) {
|
|
3004
|
+
async delete(caller, { id, revision }) {
|
|
2897
3005
|
const identity = await identityOf(caller);
|
|
2898
3006
|
const authorized = await authorizeOwn(writes, identity.principal, messageAt(id), "delete");
|
|
2899
3007
|
await writeDelete(writes, {
|
|
2900
3008
|
identity,
|
|
2901
|
-
authorized
|
|
3009
|
+
authorized,
|
|
3010
|
+
revision
|
|
2902
3011
|
});
|
|
2903
3012
|
return { ok: true };
|
|
2904
3013
|
},
|
|
@@ -2913,14 +3022,15 @@ function build(options, binding) {
|
|
|
2913
3022
|
});
|
|
2914
3023
|
return { ok: true };
|
|
2915
3024
|
},
|
|
2916
|
-
async label(caller, { id, set, unset }) {
|
|
3025
|
+
async label(caller, { id, set, unset, if: expected }) {
|
|
2917
3026
|
const identity = await identityOf(caller);
|
|
2918
3027
|
const authorized = await authorizeMessage(writes, identity.principal.access, "label", messageAt(id));
|
|
2919
3028
|
await writeLabels(writes, {
|
|
2920
3029
|
identity,
|
|
2921
3030
|
authorized,
|
|
2922
3031
|
set,
|
|
2923
|
-
unset
|
|
3032
|
+
unset,
|
|
3033
|
+
if: expected
|
|
2924
3034
|
});
|
|
2925
3035
|
return { ok: true };
|
|
2926
3036
|
},
|
|
@@ -3194,6 +3304,7 @@ function build(options, binding) {
|
|
|
3194
3304
|
origin: body.origin,
|
|
3195
3305
|
from: body.from,
|
|
3196
3306
|
...body.origin && !ownSync(caller) ? { importedBy: principal.id } : {},
|
|
3307
|
+
after: afterAt(body.after),
|
|
3197
3308
|
lenientMentions: true,
|
|
3198
3309
|
attachments,
|
|
3199
3310
|
links: await linksOf(principal, body.links)
|
|
@@ -3222,7 +3333,8 @@ function build(options, binding) {
|
|
|
3222
3333
|
authorized: proof,
|
|
3223
3334
|
text,
|
|
3224
3335
|
from: body.from,
|
|
3225
|
-
asAuthor
|
|
3336
|
+
asAuthor,
|
|
3337
|
+
revision: body.revision
|
|
3226
3338
|
});
|
|
3227
3339
|
return ref(found.id);
|
|
3228
3340
|
};
|
|
@@ -3240,7 +3352,8 @@ function build(options, binding) {
|
|
|
3240
3352
|
identity: author,
|
|
3241
3353
|
authorized: await authorized("post"),
|
|
3242
3354
|
from: body.from,
|
|
3243
|
-
asAuthor
|
|
3355
|
+
asAuthor,
|
|
3356
|
+
revision: body.revision
|
|
3244
3357
|
});
|
|
3245
3358
|
else if (body.op === "react") await writeReaction(writes, {
|
|
3246
3359
|
identity: author,
|
|
@@ -3253,7 +3366,8 @@ function build(options, binding) {
|
|
|
3253
3366
|
identity: author,
|
|
3254
3367
|
authorized: await authorized("label"),
|
|
3255
3368
|
set: body.set,
|
|
3256
|
-
unset: body.unset
|
|
3369
|
+
unset: body.unset,
|
|
3370
|
+
if: body.if
|
|
3257
3371
|
});
|
|
3258
3372
|
else await writeLinks(writes, {
|
|
3259
3373
|
identity: author,
|