@vercel/board 0.16.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/README.md +27 -27
  2. package/dist/{a2-C41hD8SF.mjs → a2-Bck5x1Kh.mjs} +969 -25
  3. package/dist/access.d.mts +1 -1
  4. package/dist/access.mjs +3 -3
  5. package/dist/{actors-DjMeZpnU.mjs → actors-BOamkEuz.mjs} +3 -73
  6. package/dist/attempts-CLG840Oj.mjs +20 -0
  7. package/dist/{auth-COzc09sn.mjs → auth-Drg9FiC2.mjs} +225 -2
  8. package/dist/backend-B-SJ7p6p.d.mts +680 -0
  9. package/dist/backend-a2/contract.d.mts +873 -2
  10. package/dist/backend-a2/contract.mjs +2 -2
  11. package/dist/backend-a2/scheduler-local.d.mts +1 -1
  12. package/dist/backend-a2/scheduler-local.mjs +1 -1
  13. package/dist/backend-a2/scheduler-qstash.d.mts +78 -1
  14. package/dist/backend-a2/scheduler-qstash.mjs +521 -1
  15. package/dist/backend-a2/scheduler-vercel.d.mts +37 -1
  16. package/dist/backend-a2/scheduler-vercel.mjs +203 -1
  17. package/dist/backend-a2/store-memory.d.mts +2 -2
  18. package/dist/backend-a2/store-memory.mjs +3 -3
  19. package/dist/backend-a2/store-postgres.d.mts +1 -1
  20. package/dist/backend-a2/store-postgres.mjs +2850 -2
  21. package/dist/backend-a2/store-redis-http.d.mts +1 -1
  22. package/dist/backend-a2/store-redis-http.mjs +490 -2
  23. package/dist/backend-a2/store-redis.d.mts +10 -2
  24. package/dist/backend-a2/store-redis.mjs +51 -2
  25. package/dist/backend-a2/store-sqlite.d.mts +1 -1
  26. package/dist/backend-a2/store-sqlite.mjs +3 -3
  27. package/dist/backend-a2.d.mts +2706 -3
  28. package/dist/backend-a2.mjs +1 -1
  29. package/dist/{backend-http-Bid7RIpC.d.mts → backend-http-7cNb8nJI.d.mts} +10 -3
  30. package/dist/{backend-http-0roEmjl0.mjs → backend-http-C5I7Wfdw.mjs} +2 -2
  31. package/dist/{backend-http-BVRno-ca.mjs → backend-http-CuUIACCs.mjs} +32 -3
  32. package/dist/backend-http.d.mts +1 -1
  33. package/dist/backend-http.mjs +1 -1
  34. package/dist/backend-http.node.d.mts +1 -1
  35. package/dist/backend-http.node.mjs +1 -1
  36. package/dist/{chat-provider-XCnQlxq6.d.mts → chat-provider-DNJQyajS.d.mts} +16 -4
  37. package/dist/{chat-provider-jFSeyNmB.mjs → chat-provider-YHy-WbrB.mjs} +15 -9
  38. package/dist/cli.mjs +30 -10
  39. package/dist/{client-DGDPaN7U.mjs → client-BA-J7usH.mjs} +10 -2
  40. package/dist/{conformance-DRR23YNy.mjs → conformance-CT5EeHeC.mjs} +5 -2
  41. package/dist/conformance.d.mts +1 -1
  42. package/dist/conformance.mjs +1 -1
  43. package/dist/{engine-Bir5s8ts.mjs → engine-D0oBUBEK.mjs} +249 -163
  44. package/dist/events-CewD4kw7.mjs +75 -0
  45. package/dist/{exports-Dvc1o52I.mjs → exports-DOdA7KhF.mjs} +35 -32
  46. package/dist/exports-ZsgjzEnM.d.mts +4 -0
  47. package/dist/external-BV_cUncs.mjs +151 -0
  48. package/dist/folds-CRYY14kc.d.mts +534 -0
  49. package/dist/{reducers-mUPFuFJV.mjs → folds-Dyjput8M.mjs} +6 -73
  50. package/dist/ids-C6AYo1bu.mjs +31 -0
  51. package/dist/index.d.mts +4 -4
  52. package/dist/{index.default-Bkg77Pxn.mjs → index.default-n4qKN7EL.mjs} +1 -1
  53. package/dist/index.mjs +5 -5
  54. package/dist/index.node.d.mts +4 -4
  55. package/dist/index.node.mjs +3 -3
  56. package/dist/inspect/app.js +31 -31
  57. package/dist/inspect/engine.js +5 -5
  58. package/dist/internal-CvsrprnU-CRY89I-b.mjs +203 -0
  59. package/dist/{local-Cr_dSjvs.mjs → local-DhkrAqPP.mjs} +1 -1
  60. package/dist/mentions-CB6ShY4R.mjs +72 -0
  61. package/dist/{targets-yM5JBUgr.mjs → middleware-CqaCwucP.mjs} +1 -33
  62. package/dist/platform-B4TnJtWu-CwcTyKg2.mjs +32 -0
  63. package/dist/{project-CFiArraX.mjs → project-DMDST9kl.mjs} +1 -1
  64. package/dist/{provider-BGjz-mDa.d.mts → provider-B0b9Qxw4.d.mts} +170 -40
  65. package/dist/provider-B3DpYSDq.mjs +220 -0
  66. package/dist/redis-tables-DN9PfIDi.mjs +3897 -0
  67. package/dist/reducer-CYPHsSmd-BQIfozGi.d.mts +170 -0
  68. package/dist/reducer-CjmvZSAb-D_OSISmj.mjs +74 -0
  69. package/dist/{contract-CuJmlrhw.mjs → reducers-BFmIXilu.mjs} +260 -287
  70. package/dist/retryable-lazy-DZWmHpii-Bye-W8q7.mjs +19 -0
  71. package/dist/scheduler-task-X1SToDjP-BVoWuInI.mjs +69 -0
  72. package/dist/{serve-BsOOc-6_.mjs → serve-CNYvA4pT.mjs} +5 -5
  73. package/dist/server-DtFrWxi3-Boa6gdWO.mjs +3602 -0
  74. package/dist/server-qgez0Xny-D5mLfqFQ.d.mts +385 -0
  75. package/dist/{server-jwPY7_wi.mjs → server-snYFkITb.mjs} +4 -4
  76. package/dist/{serving-BNbzc96L.mjs → serving-CNkgUPW1.mjs} +1 -1
  77. package/dist/session-clear-wHd5GCpZ-BnKSxReK.mjs +51 -0
  78. package/dist/{sql-DbiEnqM0.mjs → sql-U9WTxrwJ.mjs} +2 -2
  79. package/dist/{sqlite-db-Cbj03vmp.mjs → sqlite-db-Ds4majtT.mjs} +1 -1
  80. package/dist/store-D0MhBvg6-4Z2L0Kqd.d.mts +530 -0
  81. package/dist/store-N8PXxDAS-BWBgzBHp.mjs +5 -0
  82. package/dist/store-codec-DwrWlgQx-CYxGW0aD.mjs +61 -0
  83. package/dist/store-memory-Bas9Uq5C.mjs +872 -0
  84. package/dist/store-memory-ZLiZbH3R.mjs +2 -0
  85. package/dist/store-polling-BZt2Rf3D-BiEM6_HB.mjs +148 -0
  86. package/dist/store-presence-polling-JuAy2Ck2-CYpuK75X.mjs +109 -0
  87. package/dist/store-sqlite-BmwL3pUR.mjs +2 -0
  88. package/dist/store-sqlite-CxxPCcCT.mjs +1053 -0
  89. package/dist/stream-buffer-DrTyiIuo-BKy-vI14.mjs +53 -0
  90. package/dist/sync-github.d.mts +24 -17
  91. package/dist/sync-github.mjs +89 -22
  92. package/dist/sync-kapso.d.mts +15 -13
  93. package/dist/sync-kapso.mjs +17 -14
  94. package/dist/sync-slack.d.mts +12 -17
  95. package/dist/sync-slack.mjs +54 -42
  96. package/dist/sync.chat.d.mts +1 -1
  97. package/dist/sync.chat.mjs +1 -1
  98. package/dist/sync.d.mts +3 -3
  99. package/dist/sync.mjs +5 -4
  100. package/dist/targets-C-wdR5wP.mjs +33 -0
  101. package/dist/{tokens-DKLn5P9B.mjs → tokens-DhuzKzAl.mjs} +1 -1
  102. package/dist/{types-BHCjTZdB.d.mts → types-8yq2VnKI.d.mts} +2 -2
  103. package/dist/{users-Dls7_ANV.mjs → users-yryHKFMo.mjs} +2 -2
  104. package/dist/{watch-spec-W0c7ypHJ.mjs → watchers-hash-DojIuoFg.mjs} +233 -28
  105. package/docs/01-board/01-mounting.md +1 -1
  106. package/docs/02-sync/01-overview.md +69 -47
  107. package/docs/02-sync/02-functions.md +395 -0
  108. package/docs/02-sync/03-slack.md +30 -25
  109. package/docs/02-sync/04-github.md +62 -26
  110. package/docs/02-sync/05-whatsapp.md +23 -19
  111. package/docs/02-sync/06-providers.md +58 -26
  112. package/docs/03-guides/04-triage.md +1 -1
  113. package/docs/04-reference/01-cli.md +26 -6
  114. package/docs/04-reference/02-board.md +18 -5
  115. package/docs/04-reference/03-http.md +5 -2
  116. package/docs/04-reference/05-conformance.md +1 -1
  117. package/docs/index.md +5 -10
  118. package/package.json +22 -6
  119. package/dist/a2-D24cvwpN.d.mts +0 -3359
  120. package/dist/access-CJU2-vaa.mjs +0 -226
  121. package/dist/attempts-Bxp9KgPf.mjs +0 -919
  122. package/dist/exports-DLiE7fLY.d.mts +0 -4
  123. package/dist/provider-CuewcDNZ.mjs +0 -205
  124. package/dist/redis-tables-B35vv4CZ.mjs +0 -842
  125. package/dist/reducers-B_vUGQRp.d.mts +0 -1172
  126. package/dist/watchers-hash-CLjsa6od.mjs +0 -225
  127. package/docs/02-sync/02-callbacks.md +0 -265
@@ -1,919 +0,0 @@
1
- import { n as hasCriteria } from "./watch-spec-W0c7ypHJ.mjs";
2
- import { a as forbidden, i as duplicateId, r as badRequest, s as notFound, t as HttpError } from "./errors-DX3hpwWt.mjs";
3
- import { t as own } from "./own-OwgPWdmh.mjs";
4
- import { a as resolveMentions, n as actorSeen, s as replaceMentions } from "./actors-DjMeZpnU.mjs";
5
- import { n as authorize, t as FULL_ACCESS } from "./access-CJU2-vaa.mjs";
6
- import { n as syncTarget, r as withEmailMentions } from "./targets-yM5JBUgr.mjs";
7
- import { a as foldMessages, f as personOf, i as foldActors, l as iso, n as channelsReducer, t as actorsReducer, u as linkedTo } from "./reducers-mUPFuFJV.mjs";
8
- import { t as concernedMessageId } from "./project-CFiArraX.mjs";
9
- import { idempotentId } from "experimental-a2";
10
- //#region src/server/feed.ts
11
- /** Bookkeeping events, shown only with `all`: who's who, and delivery receipts. */
12
- const HIDDEN = /* @__PURE__ */ new Set([
13
- "actor.seen",
14
- "message.synced",
15
- "message.files_synced"
16
- ]);
17
- /** The person a linked platform actor is, as views show it. */
18
- function personView(actors, actor) {
19
- const link = own(actors.links, actor);
20
- const person = link ? own(actors.byId, link.to) : void 0;
21
- return link ? {
22
- id: link.to,
23
- handle: person?.handle ?? link.to,
24
- name: person?.name ?? link.to,
25
- avatar: person?.avatar ?? null
26
- } : void 0;
27
- }
28
- function actorView(actors, id) {
29
- const author = own(actors.byId, id);
30
- if (!author) return null;
31
- const person = personView(actors, id);
32
- return {
33
- kind: author.kind,
34
- handle: author.handle,
35
- name: author.name,
36
- avatar: author.avatar,
37
- ...person ? { person } : {}
38
- };
39
- }
40
- /** The board's own code, which reads every channel: it needs no messages to say so. */
41
- const OWN_CODE = {
42
- access: FULL_ACCESS,
43
- channelOf: () => void 0
44
- };
45
- /** The board messages `links` point to, for `Reader.channelOf`. */
46
- const linkedIds = (links) => links.flatMap((link) => link.message === null ? [] : [link.message]);
47
- /**
48
- * Links as `reader` may see them. The log keeps each snapshot; it's hidden when
49
- * it's read. A linked message the reader's lookup doesn't have hides too: its
50
- * snapshot shows only where it's known to be readable.
51
- */
52
- function readableLinks(links, reader) {
53
- return links.map((link) => {
54
- if (link.message === null) return {
55
- ...link,
56
- message: null
57
- };
58
- const channel = reader.access.readsAll ? void 0 : reader.channelOf(link.message);
59
- return reader.access.readsAll || channel !== void 0 && reader.access.readable(channel) ? {
60
- ...link,
61
- message: ref(link.message)
62
- } : {
63
- url: link.url,
64
- message: null,
65
- snapshot: null,
66
- unavailable: "not_readable"
67
- };
68
- });
69
- }
70
- /** The links in an event view: its payload's, its message's, and the message before a label change. */
71
- const viewLinks = (view) => {
72
- const { payload, message, before } = view;
73
- return [
74
- payload?.links,
75
- message?.links,
76
- before?.links
77
- ].filter((links) => links !== void 0);
78
- };
79
- /** The board messages an event view's links point to, for a reader of them. */
80
- const viewLinkedIds = (view) => viewLinks(view).flatMap((links) => links.flatMap((link) => link.message === null ? [] : [indexOf(link.message) ?? 0]));
81
- /**
82
- * An event view made for one reader, as `reader` sees it: a link to a message it can't read shows
83
- * nothing of it. A watcher's match is made when its event lands and pulled later, by whatever its
84
- * registrant may read then.
85
- */
86
- function viewedBy(view, reader) {
87
- if (reader.access.readsAll) return view;
88
- const shown = structuredClone(view);
89
- const reread = (links) => readableLinks(links.map((link) => ({
90
- ...link,
91
- message: link.message === null ? null : indexOf(link.message) ?? 0
92
- })), reader);
93
- if (shown.payload?.links) shown.payload.links = reread(shown.payload.links);
94
- if (shown.message) shown.message.links = reread(shown.message.links);
95
- if (shown.before) shown.before.links = reread(shown.before.links);
96
- return shown;
97
- }
98
- /** A message's or a cursor's ID as clients see it. On this board it's the log index, in decimal. */
99
- const ref = (index) => String(index);
100
- /** The log index a ref names on this board, or null for a string that names nothing here. */
101
- const indexOf = (value) => /^(0|[1-9]\d{0,15})$/.test(value) ? Number(value) : null;
102
- /** A message as `reader` sees it. Every view names its reader, so none shows a link's snapshot by default. */
103
- function view(message, actors, reader) {
104
- const { blind, version, schedule, ...shown } = message;
105
- return {
106
- ...shown,
107
- ...blind ? { blind } : {},
108
- id: ref(message.id),
109
- re: message.re === null ? null : ref(message.re),
110
- replyTo: message.replyTo === null ? null : ref(message.replyTo),
111
- version: version ?? message.id,
112
- ...schedule ? { schedule: {
113
- id: ref(schedule.id),
114
- n: schedule.n
115
- } } : {},
116
- links: readableLinks(message.links, reader),
117
- labels: { ...message.labels },
118
- reactions: Object.fromEntries(Object.entries(message.reactions).map(([emoji, ids]) => [emoji, [...ids]])),
119
- author: actorView(actors, message.by.actor)
120
- };
121
- }
122
- /** Whether accounts are linked to `id`: then it's a person, even one who never wrote on the board. */
123
- const hasLinks = (actors, id) => Object.values(actors.links).some((link) => link.to === id);
124
- const actorSet = (value, actors) => own(actors.byId, value) || own(actors.links, value) || hasLinks(actors, value) ? /* @__PURE__ */ new Set([value]) : new Set(own(actors.byHandle, value.toLowerCase()) ?? []);
125
- /**
126
- * Who a `--by` or `--mentions` value covers. A person (`julian`, or one of their
127
- * linked accounts, `@julian` on Slack) is everyone they are: their own actor,
128
- * their agents (`julian/claude`), and every account linked to them. An agent
129
- * or an unlinked platform actor is just itself.
130
- */
131
- function whoIs(value, actors) {
132
- const ids = actorSet(value, actors);
133
- const out = new Set(ids);
134
- const principals = /* @__PURE__ */ new Set();
135
- for (const id of ids) {
136
- const kind = own(actors.byId, id)?.kind;
137
- const person = own(actors.links, id) ? personOf(actors, id) : kind === "person" || hasLinks(actors, id) ? id : null;
138
- if (person === null) continue;
139
- principals.add(person);
140
- out.add(person);
141
- for (const linked of linkedTo(actors, person)) out.add(linked);
142
- }
143
- return {
144
- actors: out,
145
- principals
146
- };
147
- }
148
- /** Whether `author` is one of `who`: its actor, or an agent of one of its people. */
149
- const covers = (who, actor, principal) => who.actors.has(actor) || principal !== void 0 && who.principals.has(principal);
150
- const hasLabel = (labels, [key, values]) => Object.hasOwn(labels, key) && (values === null || values.includes(labels[key]));
151
- /**
152
- * Compiles filters once; handles resolve against the actors known at that
153
- * moment. A label matches on the message or its thread's root, where sync puts
154
- * labels, so `pr=…` means the PR's whole conversation. `messages` is the live
155
- * state for that lookup. A message in a channel `access` can't read never
156
- * matches, whatever the filters say.
157
- */
158
- function compileFilter(filters, context) {
159
- const { access } = context;
160
- const text = filters.text?.toLowerCase();
161
- const labels = filters.label ?? [];
162
- const channel = filters.channel;
163
- const subtree = channel?.endsWith("/") ? channel.slice(0, -1) : null;
164
- const by = filters.by === void 0 ? null : whoIs(filters.by, context.actors);
165
- const re = filters.re === void 0 ? void 0 : indexOf(filters.re) ?? -1;
166
- const mentions = filters.mentions === void 0 ? null : filters.mentions === "me" ? new Set(context.me === null ? [] : [context.me, ...linkedTo(context.actors, context.me)]) : whoIs(filters.mentions, context.actors).actors;
167
- return (message) => {
168
- if (!message || !access.readable(message.channel)) return false;
169
- if (channel !== void 0) {
170
- if (subtree !== null) {
171
- if (message.channel !== subtree && !message.channel.startsWith(`${subtree}/`)) return false;
172
- } else if (message.channel !== channel) return false;
173
- }
174
- if (re !== void 0 && message.id !== re && message.re !== re) return false;
175
- if (by !== null && !covers(by, message.by.actor, message.by.principal)) return false;
176
- if (mentions !== null && !message.mentions.some((id) => mentions.has(id))) return false;
177
- if (filters.from !== void 0 && (message.from ?? "board") !== filters.from) return false;
178
- if (text !== void 0 && !message.text.toLowerCase().includes(text)) return false;
179
- if (labels.length > 0) {
180
- const root = message.re === null ? void 0 : context.messages?.byId[message.re];
181
- for (const label of labels) if (!hasLabel(message.labels, label) && !(root && hasLabel(root.labels, label))) return false;
182
- }
183
- return true;
184
- };
185
- }
186
- /**
187
- * `filter`, narrowed to the channels `access` reads among `known`, as the tables
188
- * take it: a reader of every channel needs nothing more. A channel the caller
189
- * didn't know of is left out, never let in.
190
- */
191
- function readableFilter(filter, known, access) {
192
- if (access.readsAll) return filter;
193
- const { channel, ...rest } = filter;
194
- const within = (path) => !channel || path === channel.path || channel.subtree && path.startsWith(`${channel.path}/`);
195
- return {
196
- ...rest,
197
- channels: [...known].filter((path) => within(path) && access.readable(path))
198
- };
199
- }
200
- /** The filters as the tables take them: handles resolved to who they cover, `me` to the caller. */
201
- function toMessageFilter(filters, context) {
202
- const out = {};
203
- const channel = filters.channel;
204
- if (channel !== void 0) out.channel = channel.endsWith("/") ? {
205
- path: channel.slice(0, -1),
206
- subtree: true
207
- } : {
208
- path: channel,
209
- subtree: false
210
- };
211
- if (filters.re !== void 0) out.re = indexOf(filters.re) ?? -1;
212
- if (filters.by !== void 0) {
213
- const who = whoIs(filters.by, context.actors);
214
- out.by = {
215
- actors: [...who.actors],
216
- principals: [...who.principals]
217
- };
218
- }
219
- if (filters.mentions !== void 0) out.mentions = filters.mentions === "me" ? context.me === null ? [] : [context.me, ...linkedTo(context.actors, context.me)] : [...whoIs(filters.mentions, context.actors).actors];
220
- if (filters.from !== void 0) out.from = filters.from;
221
- if (filters.text !== void 0) out.text = filters.text.toLowerCase();
222
- if (filters.label?.length) out.labels = filters.label.map(([key, values]) => ({
223
- key,
224
- values
225
- }));
226
- return out;
227
- }
228
- /**
229
- * Loads into `messages`, as they were just before `event`, the message it
230
- * concerns and that message's root: what folding it and matching filters need.
231
- * `loaded` remembers what was asked for already, found or not.
232
- */
233
- async function loadInto(reads, messages, loaded, event, signal) {
234
- const id = concernedMessageId(event);
235
- if (id === null) return;
236
- const through = event.index - 1;
237
- const load = async (ids) => {
238
- const wanted = ids.filter((want) => !loaded.has(want));
239
- if (wanted.length === 0) return;
240
- const found = await reads.messagesAt(wanted, through, signal);
241
- for (const want of wanted) {
242
- loaded.add(want);
243
- const message = found.get(want);
244
- if (message) messages.byId[want] = message;
245
- }
246
- };
247
- if (event.type === "message.posted") loaded.add(id);
248
- else await load([id]);
249
- const re = event.type === "message.posted" ? event.payload.re : messages.byId[id]?.re ?? null;
250
- if (re !== null) await load([re]);
251
- }
252
- /** The message `event` concerns and its root, as they were just before it. */
253
- async function loadBefore(reads, event, signal) {
254
- const messages = {
255
- byId: {},
256
- replies: {}
257
- };
258
- await loadInto(reads, messages, /* @__PURE__ */ new Set(), event, signal);
259
- return messages;
260
- }
261
- /** Folds one event into `messages` and `actors`, and returns the message it concerns before and after. */
262
- function foldEvent(messages, actors, event) {
263
- const id = concernedMessageId(event);
264
- const before = id === null ? void 0 : structuredClone(messages.byId[id]);
265
- foldMessages(messages, event);
266
- foldActors(actors, event);
267
- return {
268
- id,
269
- before,
270
- after: id === null ? void 0 : messages.byId[id]
271
- };
272
- }
273
- /** An event as subscribers receive it, with its message as it is afterwards. */
274
- function eventView(event, after, actors, reader) {
275
- const by = event.payload.by?.actor;
276
- const actor = by ? own(actors.byId, by) : void 0;
277
- const { links } = event.payload;
278
- return {
279
- cursor: ref(event.index),
280
- type: event.type,
281
- createdAt: iso(event.createdAt),
282
- payload: payloadView(event.payload, links ? readableLinks(links, reader) : void 0),
283
- ...after ? { message: view(after, actors, reader) } : {},
284
- ...actor ? { actor: {
285
- id: actor.id,
286
- ...actorView(actors, actor.id)
287
- } } : {}
288
- };
289
- }
290
- /** An event's payload as clients see it: the message IDs in it are refs. */
291
- function payloadView(payload, links) {
292
- const shown = { ...payload };
293
- for (const key of [
294
- "id",
295
- "re",
296
- "replyTo"
297
- ]) if (typeof shown[key] === "number") shown[key] = ref(shown[key]);
298
- const schedule = shown.schedule;
299
- if (schedule) shown.schedule = {
300
- id: ref(schedule.id),
301
- n: schedule.n
302
- };
303
- if (links) shown.links = links;
304
- return shown;
305
- }
306
- /**
307
- * Folds events one at a time and decides what a subscriber sees. It holds only
308
- * the messages events have touched, each loaded from the tables as it was just
309
- * before the first event that touched it. A label or reaction change is
310
- * delivered when its message matched the filter before *or* after it, so
311
- * subscribers see messages leave a view too.
312
- *
313
- * It's the one choke point for `/events` (tail, wait, and poll): an event about
314
- * a message shows only where `access` reads its channel, and one about no
315
- * message (who's who, a thread's sync) only to a reader of every channel.
316
- */
317
- var FeedProjection = class {
318
- messages = {
319
- byId: {},
320
- replies: {}
321
- };
322
- loaded = /* @__PURE__ */ new Set();
323
- reads;
324
- actors;
325
- match;
326
- criteria;
327
- all;
328
- access;
329
- /** Where each board message a view links to lives, for a reader that can't read every channel. */
330
- linked = /* @__PURE__ */ new Map();
331
- constructor(options) {
332
- this.reads = options.reads;
333
- this.actors = options.actors;
334
- this.access = options.access;
335
- this.match = compileFilter(options.filters, {
336
- me: options.me,
337
- actors: options.actors,
338
- messages: this.messages,
339
- access: options.access
340
- });
341
- this.criteria = hasCriteria(options.filters);
342
- this.all = options.all;
343
- }
344
- /** Loads what `events` touch, all as of `index`, in two queries rather than two per event. */
345
- async preload(events, index) {
346
- const ids = /* @__PURE__ */ new Set();
347
- for (const event of events) {
348
- const id = concernedMessageId(event);
349
- if (id === null) continue;
350
- if (event.type !== "message.posted") ids.add(id);
351
- else if (event.payload.re !== null) ids.add(event.payload.re);
352
- }
353
- const load = async (wanted) => {
354
- if (wanted.length === 0) return;
355
- const found = await this.reads.messagesAt(wanted, index);
356
- for (const id of wanted) {
357
- this.loaded.add(id);
358
- const message = found.get(id);
359
- if (message) this.messages.byId[id] = message;
360
- }
361
- };
362
- await load([...ids].filter((id) => !this.loaded.has(id) && id <= index));
363
- const roots = Object.values(this.messages.byId).flatMap((message) => message.re === null ? [] : [message.re]);
364
- await load([...new Set(roots)].filter((id) => !this.loaded.has(id)));
365
- }
366
- async step(event, signal) {
367
- await loadInto(this.reads, this.messages, this.loaded, event, signal);
368
- const { id, before, after } = foldEvent(this.messages, this.actors, event);
369
- const message = after ?? before;
370
- const readable = id === null ? this.access.readsAll : message !== void 0 && this.access.readable(message.channel);
371
- const hidden = HIDDEN.has(event.type) && !this.all;
372
- if (!(readable && !hidden && (this.criteria ? this.match(before) || this.match(after) : id !== null || this.all))) return {
373
- index: event.index,
374
- view: null
375
- };
376
- await this.loadLinked([...after?.links ?? [], ...event.payload.links ?? []], event.index, signal);
377
- const reader = {
378
- access: this.access,
379
- channelOf: (message) => this.messages.byId[message]?.channel ?? this.linked.get(message)
380
- };
381
- return {
382
- index: event.index,
383
- view: eventView(event, after, this.actors, reader)
384
- };
385
- }
386
- /** Where the messages `links` point to live; a message never moves, so any version says. */
387
- async loadLinked(links, index, signal) {
388
- if (this.access.readsAll) return;
389
- const wanted = [...new Set(linkedIds(links))].filter((id) => !this.messages.byId[id] && !this.linked.has(id));
390
- if (wanted.length === 0) return;
391
- const found = await this.reads.messagesAt(wanted, index, signal);
392
- for (const id of wanted) this.linked.set(id, found.get(id)?.channel);
393
- }
394
- };
395
- /** Resolves when the iterator yields, or with null once the signal aborts. */
396
- function nextOrAbort(iterator, signal) {
397
- if (signal.aborted) return Promise.resolve(null);
398
- return new Promise((resolve, reject) => {
399
- const onAbort = () => resolve(null);
400
- signal.addEventListener("abort", onAbort, { once: true });
401
- iterator.next().then((result) => {
402
- signal.removeEventListener("abort", onAbort);
403
- resolve(result);
404
- }, (error) => {
405
- signal.removeEventListener("abort", onAbort);
406
- reject(error);
407
- });
408
- });
409
- }
410
- /** Where a feed starts: `after`, or the head when it's omitted or past it. */
411
- async function start(options) {
412
- const { index: head } = await options.session.state(channelsReducer);
413
- return options.after === void 0 || options.after >= head ? head : options.after;
414
- }
415
- const PAGE = 200;
416
- /**
417
- * One long-poll: matching events already in the log return at once; otherwise
418
- * it waits up to `waitMs` for the first new match. The cursor is the last
419
- * event examined, matched or not, so the next poll never rescans.
420
- */
421
- async function pollFeed(options, waitMs, signal) {
422
- const index = await start(options);
423
- const feed = new FeedProjection(options);
424
- const events = [];
425
- let cursor = index;
426
- const backlog = await options.session.history({
427
- gte: index + 1,
428
- lte: index + PAGE
429
- });
430
- await feed.preload(backlog, index);
431
- for (const event of backlog) {
432
- const item = await feed.step(event, signal);
433
- cursor = item.index;
434
- if (item.view) events.push(item.view);
435
- }
436
- if (events.length > 0 || backlog.length === PAGE || waitMs <= 0) return {
437
- events,
438
- cursor
439
- };
440
- const timeout = AbortSignal.timeout(waitMs);
441
- const until = AbortSignal.any([signal, timeout]);
442
- const iterator = options.session.stream({ startAfter: cursor })[Symbol.asyncIterator]();
443
- try {
444
- while (true) {
445
- const next = await nextOrAbort(iterator, until);
446
- if (next === null || next.done) return {
447
- events,
448
- cursor
449
- };
450
- const item = await feed.step(next.value, until);
451
- cursor = item.index;
452
- if (item.view) {
453
- events.push(item.view);
454
- return {
455
- events,
456
- cursor
457
- };
458
- }
459
- }
460
- } finally {
461
- iterator.return?.();
462
- }
463
- }
464
- //#endregion
465
- //#region src/server/wait-until.ts
466
- /**
467
- * Keeps `task` running after the response, where the platform offers it: Vercel's request context, as A2
468
- * reads it too. Elsewhere it runs on in the process.
469
- */
470
- function waitUntil(task) {
471
- try {
472
- Reflect.get(globalThis, Symbol.for("@vercel/request-context"))?.get?.()?.waitUntil?.(task);
473
- } catch {}
474
- }
475
- //#endregion
476
- //#region src/server/writes.ts
477
- /** A piece of the board's own deployment, writing as `actor` with full access. */
478
- function deploymentIdentity(actor, handle) {
479
- return {
480
- principal: {
481
- id: actor,
482
- kind: "deployment",
483
- handle,
484
- access: FULL_ACCESS,
485
- expiresAt: null
486
- },
487
- author: {
488
- actor,
489
- principal: actor,
490
- profile: null,
491
- session: null
492
- },
493
- actor: {
494
- id: actor,
495
- kind: "deployment",
496
- handle,
497
- name: handle,
498
- avatar: null
499
- }
500
- };
501
- }
502
- /**
503
- * The `actor.seen` a write carries, or null: who its author is, as the board's record of them
504
- * says, unless the board knows that already. A platform user's record is board-wide, so a write
505
- * whose identity doesn't keep it (`records: false`) leaves it as it is. A host's principal and its
506
- * profiles are recorded as the host's, which sync never writes as.
507
- */
508
- function seenFor(identity, actors) {
509
- if (identity.records === false) return null;
510
- return actorSeen(identity.actor, actors, identity.principal.facts?.issuer === "host");
511
- }
512
- /** The actors a write can mention: the ones the board knows, plus the ones an email named. */
513
- const knownWith = (actors, found) => found.length === 0 ? actors : {
514
- ...actors,
515
- byId: {
516
- ...actors.byId,
517
- ...Object.fromEntries(found.map((a) => [a.id, a]))
518
- }
519
- };
520
- const messageNotFound = (id) => () => notFound(`message not found: #${id}`, "message_not_found");
521
- /**
522
- * A message the board looked up and the caller may act on with `permission`,
523
- * proved against its own channel. A message they can't read answers 404, as if
524
- * it didn't exist: IDs are sequential, so a 403 would confirm it does.
525
- */
526
- async function authorizeMessage(ctx, access, permission, id) {
527
- const message = await ctx.reads.message(id);
528
- if (!message) throw messageNotFound(id)();
529
- return {
530
- message,
531
- proof: authorize(access, permission, message.channel, messageNotFound(id))
532
- };
533
- }
534
- /**
535
- * Your own message, to edit or delete: `post` in its channel. Where you can't
536
- * read, it's yours only if it was posted without reading too: a write-only
537
- * source fixes what it wrote, but a principal ID it shares with a caller that
538
- * reads (a repo's workflows share one) doesn't reach that caller's messages.
539
- * Someone else's answers 404 where you can't read it, and 403 where you can.
540
- */
541
- async function authorizeOwn(ctx, principal, id, verb) {
542
- const message = await ctx.reads.message(id);
543
- if (!message) throw messageNotFound(id)();
544
- const readable = principal.access.readable(message.channel);
545
- if (message.by.principal !== principal.id || !readable && !message.blind) {
546
- if (!readable) throw messageNotFound(id)();
547
- throw forbidden(`only the author can ${verb} a message`);
548
- }
549
- return {
550
- message,
551
- proof: authorize(principal.access, "post", message.channel)
552
- };
553
- }
554
- const hasLabels = (labels) => Object.keys(labels ?? {}).length > 0;
555
- /**
556
- * Proofs for a post. A root lands in the channel it names, which the caller
557
- * needn't read: write-only is `['post']`. A reply lands in its root's channel,
558
- * looked up here and never taken from the request, and needs `read` there too,
559
- * since addressing a thread you can't see would confirm it exists.
560
- */
561
- async function authorizePost(ctx, access, input) {
562
- let channel;
563
- let re;
564
- if (input.re !== void 0) {
565
- re = (await authorizeMessage(ctx, access, "read", input.re)).message;
566
- channel = re.channel;
567
- } else {
568
- if (!input.channel) throw badRequest("a post needs a channel");
569
- channel = input.channel;
570
- }
571
- return {
572
- post: authorize(access, "post", channel),
573
- ...re ? { re } : {},
574
- ...hasLabels(input.labels) ? { label: authorize(access, "label", channel) } : {},
575
- ...input.sync?.length ? { sync: authorize(access, "sync", channel) } : {}
576
- };
577
- }
578
- /**
579
- * Spends a proof on its target: a write checked against one channel can't land
580
- * in another. A mismatch is the board's own bug, never the caller's.
581
- */
582
- function spend(proof, permission, target) {
583
- if (!proof || proof.permission !== permission || proof.channel !== target.channel) throw new Error(`a ${permission} write in ${target.channel} needs a proof for that channel`);
584
- }
585
- /** Full-access proofs for the board's own code (sync), still checked against the channel each write lands in. */
586
- const trusted = {
587
- message: (ctx, permission, id) => authorizeMessage(ctx, FULL_ACCESS, permission, id),
588
- post: (ctx, input) => authorizePost(ctx, FULL_ACCESS, input)
589
- };
590
- async function writePost(ctx, input) {
591
- const { identity } = input;
592
- const sent = input.text;
593
- let channel = input.channel;
594
- let re = null;
595
- let replyTo = null;
596
- let thread;
597
- const id = input.id ?? (input.re === void 0 ? crypto.randomUUID() : void 0);
598
- if (input.re !== void 0) {
599
- const target = input.proofs.re;
600
- if (target?.id !== input.re) throw new Error(`a reply to #${input.re} needs proofs for that message`);
601
- re = target.re ?? target.id;
602
- replyTo = target.id === re ? null : target.id;
603
- channel = target.channel;
604
- thread = target.thread;
605
- if (input.sync?.length) throw badRequest("sync attaches whole threads: use `board sync <id> <target>`");
606
- } else {
607
- if (!channel) throw badRequest("a post needs a channel");
608
- thread = id;
609
- }
610
- spend(input.proofs.post, "post", { channel });
611
- if (hasLabels(input.labels)) spend(input.proofs.label, "label", { channel });
612
- if (input.sync?.length) spend(input.proofs.sync, "sync", { channel });
613
- if (input.id !== void 0) {
614
- const landed = await ctx.reads.byEventId(input.id);
615
- if (landed) {
616
- if (landed.channel !== channel || landed.re !== re || landed.replyTo !== replyTo) throw duplicateId();
617
- const shown = await landedFor(ctx, identity, landed);
618
- if (!shown) throw duplicateId();
619
- return shownTo(shown, identity.principal.access, sent);
620
- }
621
- }
622
- return commitPost(ctx, input, {
623
- channel,
624
- re,
625
- replyTo,
626
- thread,
627
- id,
628
- sent
629
- });
630
- }
631
- /**
632
- * A post that landed, as its author may have it back: only the same author's, and one in a
633
- * channel it can't read only if it posted without reading too. A caller that can't read gets back
634
- * what it sent, as it landed: never what others have added since. Null for anyone else.
635
- */
636
- async function landedFor(ctx, identity, landed) {
637
- if (!sameAuthor(landed.by, identity.author)) return null;
638
- const { access } = identity.principal;
639
- const { state: actors } = await ctx.session.state(actorsReducer);
640
- if (access.readable(landed.channel)) return view(landed, actors, await readerFor(ctx, access, landed));
641
- if (!landed.blind) return null;
642
- const [event] = await ctx.session.history({
643
- gte: landed.id,
644
- lte: landed.id
645
- });
646
- const posted = foldMessages({
647
- byId: {},
648
- replies: {}
649
- }, event).byId[landed.id];
650
- return {
651
- ...view(posted, actors, await readerFor(ctx, access, posted)),
652
- text: blindText(posted.text, identity.actor.id),
653
- mentions: []
654
- };
655
- }
656
- /**
657
- * A message's text for its writer where it can't read: each exact mention but of itself shows as
658
- * `@…`. The board may have turned an email it wrote into an actor ID, and who that is, `/actors`
659
- * doesn't show it either.
660
- */
661
- const blindText = (text, own) => replaceMentions(text, (token) => token.kind === "actor" && token.actor !== own ? "@…" : null);
662
- const sameAuthor = (a, b) => a.actor === b.actor && a.principal === b.principal && a.profile === b.profile && a.session === b.session;
663
- async function commitPost(ctx, input, placed) {
664
- const { identity } = input;
665
- const { channel, re, replyTo, thread, id, sent } = placed;
666
- const { state: actors } = await ctx.session.state(actorsReducer);
667
- const targets = [];
668
- if (re === null) {
669
- const originChannel = input.origin ? input.origin.channel ?? channelOf(input.origin.thread) : null;
670
- if (!input.noSync) {
671
- for (const target of input.config ?? []) if (target.target !== originChannel) targets.push({
672
- ...target,
673
- cause: "config"
674
- });
675
- }
676
- for (const requested of input.sync ?? []) {
677
- const target = syncTarget(requested);
678
- await unclaimed(ctx, target, thread, identity.principal.access);
679
- if (!targets.some((t) => t.target === target.target)) targets.push({
680
- ...target,
681
- cause: "request"
682
- });
683
- }
684
- }
685
- const seen = seenFor(identity, actors);
686
- const { text, found } = await withEmailMentions(input.text);
687
- const named = found.map((actor) => actorSeen(actor, actors)).filter((event) => event !== null);
688
- const { mentions, unknown } = resolveMentions(text, knownWith(actors, found), seen ? identity.actor : void 0, input.lenientMentions ?? false, identity.principal.access.readsAll);
689
- const synced = await Promise.all(targets.map(async ({ target, kind, cause }) => ({
690
- type: "thread.synced",
691
- id: await idempotentId("board", thread, "sync", target),
692
- payload: {
693
- thread,
694
- target,
695
- kind,
696
- cause,
697
- by: identity.author
698
- }
699
- })));
700
- const rows = await ctx.session.append(...seen ? [seen] : [], ...named, {
701
- type: "message.posted",
702
- ...id ? { id } : {},
703
- payload: {
704
- channel,
705
- text,
706
- re,
707
- replyTo,
708
- thread,
709
- labels: input.labels ?? {},
710
- noSync: input.noSync ?? false,
711
- mentions,
712
- by: identity.author,
713
- origin: input.origin ?? null,
714
- from: input.from ?? null,
715
- ...input.attachments?.length ? { attachments: input.attachments } : {},
716
- ...input.links?.length ? { links: input.links } : {},
717
- ...input.schedule ? { schedule: input.schedule } : {},
718
- ...identity.principal.access.readable(channel) ? {} : { blind: true },
719
- ...input.importedBy !== void 0 ? { importedBy: input.importedBy } : {}
720
- }
721
- }, ...synced);
722
- const posted = rows.find((row) => row.type === "message.posted");
723
- await untilApplied(ctx, rows);
724
- const message = foldMessages({
725
- byId: {},
726
- replies: {}
727
- }, posted).byId[posted.index];
728
- const known = seen ? {
729
- ...actors,
730
- byId: {
731
- ...actors.byId,
732
- [identity.actor.id]: identity.actor
733
- }
734
- } : actors;
735
- return shownTo(view(message, known, await readerFor(ctx, identity.principal.access, message)), identity.principal.access, sent, unknown);
736
- }
737
- /**
738
- * A post's view for its writer. One that can't read the channel gets back the
739
- * text it sent, no mentions, and no `unknownMentions`: the board resolved its
740
- * handles and emails to actor IDs, which `/actors` doesn't show it, and which
741
- * handles matched no one would say which ones exist. The board keeps the
742
- * mentions, so they still notify.
743
- */
744
- const shownTo = (shown, access, sent, unknown = []) => !access.readable(shown.channel) ? {
745
- ...shown,
746
- text: sent,
747
- mentions: []
748
- } : unknown.length ? {
749
- ...shown,
750
- unknownMentions: unknown
751
- } : shown;
752
- /** The writer, as the reader of the view its write returns. Only a link to a board message needs a lookup. */
753
- const readerFor = (ctx, access, message) => ctx.reads.reader(access, [message]);
754
- /**
755
- * Refuses an external thread another board thread is synced with: taking it
756
- * over would send its replies away from that thread, to a caller who may not
757
- * read it. A reader of that thread's channel learns which thread it is. It
758
- * reads the tables once they've applied the whole log, so a sync just made
759
- * counts.
760
- */
761
- async function unclaimed(ctx, target, thread, access) {
762
- if (target.kind !== "thread") return;
763
- await ctx.reads.ensure();
764
- const owner = await ctx.reads.tables.threadOf(target.target);
765
- if (owner === null || owner === thread) return;
766
- const synced = await ctx.reads.tables.syncOf(owner);
767
- const which = synced && access.readable(synced.channel) ? `#${synced.root}` : "another thread";
768
- throw new HttpError(409, "already_synced", `${target.target} is already synced with ${which}`);
769
- }
770
- /** `slack:C0123ABC:1727.1` → `slack:C0123ABC`; `github:o/r:12` → `github:o/r`. */
771
- const channelOf = (externalThread) => {
772
- const at = externalThread.indexOf(":");
773
- const colon = externalThread.slice(at + 1).indexOf(":");
774
- return colon === -1 ? externalThread : externalThread.slice(0, at + 1 + colon);
775
- };
776
- /**
777
- * Waits until the tables hold a write, so any read after it includes it. Most often
778
- * nothing was written in between, and the writer applies its own events itself; else
779
- * it waits for the tables' consumer. The write landed either way: if the tables are
780
- * slow or stuck, it still succeeds.
781
- */
782
- async function untilApplied(ctx, rows) {
783
- const events = [...rows].sort((a, b) => a.index - b.index);
784
- const last = events.at(-1).index;
785
- try {
786
- let caughtUp = true;
787
- for (const event of events) if (!(caughtUp = await ctx.reads.tables.applyNext(event))) break;
788
- if (caughtUp) return;
789
- } catch {}
790
- await ctx.reads.ensure(last, AbortSignal.timeout(LANDED_WAIT_MS)).catch((error) => {
791
- console.warn(`[board] the tables haven't applied #${last} yet:`, error.message);
792
- });
793
- }
794
- /** How long a write waits for the tables before it answers anyway. */
795
- const LANDED_WAIT_MS = 1e4;
796
- /** The message a by-ID write targets, as the board looked it up, once its proof is spent on the message's channel. */
797
- function spent(authorized, permission) {
798
- spend(authorized.proof, permission, authorized.message);
799
- return authorized.message;
800
- }
801
- async function writeEdit(ctx, input) {
802
- const message = spent(input.authorized, "post");
803
- if (input.asAuthor !== false && message.by.principal !== input.identity.principal.id) throw forbidden("only the author can edit a message");
804
- if (message.deleted) throw badRequest("message was deleted", "message_deleted");
805
- return commitEdit(ctx, input, message);
806
- }
807
- async function commitEdit(ctx, input, message) {
808
- const { state: actors } = await ctx.session.state(actorsReducer);
809
- const seen = seenFor(input.identity, actors);
810
- const { text, found } = await withEmailMentions(input.text);
811
- const named = found.map((actor) => actorSeen(actor, actors)).filter((event) => event !== null);
812
- const { mentions, unknown } = resolveMentions(text, knownWith(actors, found), seen ? input.identity.actor : void 0, input.from != null, input.identity.principal.access.readsAll);
813
- await untilApplied(ctx, await ctx.session.append(...seen ? [seen] : [], ...named, {
814
- type: "message.edited",
815
- payload: {
816
- id: message.id,
817
- thread: message.thread,
818
- text,
819
- mentions,
820
- by: input.identity.author,
821
- from: input.from ?? null
822
- }
823
- }));
824
- return unknown.length && input.identity.principal.access.readable(message.channel) ? { unknownMentions: unknown } : {};
825
- }
826
- /** A message's links, replaced: a platform edit or unfurl changed what it links to. */
827
- async function writeLinks(ctx, input) {
828
- const message = spent(input.authorized, "post");
829
- if (message.deleted) return;
830
- await untilApplied(ctx, await ctx.session.append({
831
- type: "message.linked",
832
- payload: {
833
- id: message.id,
834
- thread: message.thread,
835
- links: input.links,
836
- by: input.identity.author,
837
- from: input.from
838
- }
839
- }));
840
- }
841
- async function writeDelete(ctx, input) {
842
- const message = spent(input.authorized, "post");
843
- if (input.asAuthor !== false && message.by.principal !== input.identity.principal.id) throw forbidden("only the author can delete a message");
844
- if (message.deleted) return;
845
- await untilApplied(ctx, await ctx.session.append({
846
- type: "message.deleted",
847
- payload: {
848
- id: message.id,
849
- thread: message.thread,
850
- by: input.identity.author,
851
- from: input.from ?? null
852
- }
853
- }));
854
- }
855
- async function writeReaction(ctx, input) {
856
- const message = spent(input.authorized, "react");
857
- if (message.deleted) throw badRequest("message was deleted", "message_deleted");
858
- const { state: actors } = await ctx.session.state(actorsReducer);
859
- const seen = seenFor(input.identity, actors);
860
- await untilApplied(ctx, await ctx.session.append(...seen ? [seen] : [], {
861
- type: input.remove ? "reaction.removed" : "reaction.added",
862
- payload: {
863
- id: message.id,
864
- thread: message.thread,
865
- emoji: input.emoji,
866
- by: input.identity.author,
867
- from: input.from ?? null
868
- }
869
- }));
870
- }
871
- async function writeLabels(ctx, input) {
872
- const message = spent(input.authorized, "label");
873
- if (message.deleted) throw badRequest("message was deleted", "message_deleted");
874
- const { state: actors } = await ctx.session.state(actorsReducer);
875
- const seen = seenFor(input.identity, actors);
876
- await untilApplied(ctx, await ctx.session.append(...seen ? [seen] : [], {
877
- type: "labels.changed",
878
- payload: {
879
- id: message.id,
880
- thread: message.thread,
881
- set: input.set ?? {},
882
- unset: input.unset ?? [],
883
- by: input.identity.author
884
- }
885
- }));
886
- }
887
- /**
888
- * Syncs a message's thread with a target, resolved by the sync that delivers it. Attaching twice
889
- * is a no-op; a platform thread another board thread is synced with is a 409.
890
- */
891
- async function writeAttach(ctx, input) {
892
- const message = spent(input.authorized, "sync");
893
- const { target, kind } = input.target;
894
- await ctx.reads.ensure();
895
- if ((await ctx.reads.tables.syncOf(message.thread))?.targets.includes(target)) return { attached: false };
896
- await unclaimed(ctx, input.target, message.thread, input.identity.principal.access);
897
- await untilApplied(ctx, await ctx.session.append({
898
- type: "thread.synced",
899
- id: await idempotentId("board", message.thread, "sync", target),
900
- payload: {
901
- thread: message.thread,
902
- target,
903
- kind,
904
- cause: input.cause,
905
- by: input.identity.author
906
- }
907
- }));
908
- return { attached: true };
909
- }
910
- /**
911
- * Whether a run comes after a retry from a dead letter. A2 keeps counting
912
- * `ctx.attempt` across one, and no earlier run sees an attempt past A2_TRIES:
913
- * the event is dead by then. board gives up early, and records why, only in an
914
- * event's first tries; a retried one gets A2's fresh tries, and dead-letters
915
- * again if they all fail.
916
- */
917
- const afterRetry = (attempt) => attempt > 10;
918
- //#endregion
919
- export { view as A, linkedIds as C, readableFilter as D, pollFeed as E, viewedBy as M, ref as O, indexOf as S, personView as T, waitUntil as _, blindText as a, eventView as b, landedFor as c, writeDelete as d, writeEdit as f, writeReaction as g, writePost as h, authorizePost as i, viewLinkedIds as j, toMessageFilter as k, trusted as l, writeLinks as m, authorizeMessage as n, channelOf as o, writeLabels as p, authorizeOwn as r, deploymentIdentity as s, afterRetry as t, writeAttach as u, OWN_CODE as v, loadBefore as w, foldEvent as x, compileFilter as y };