@vercel/board 0.28.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-B0_VlbtC.mjs → a2-D_28Pdip.mjs} +197 -259
- package/dist/access-CQ39gqEV.mjs +226 -0
- package/dist/access.d.mts +1 -1
- package/dist/access.mjs +3 -3
- package/dist/{actors-BLXVp4oZ.mjs → actors-xPiamLWk.mjs} +3 -3
- package/dist/{auth-lTkOgvcE.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 +3 -3
- 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 +20 -7
- package/dist/backend-a2.mjs +2 -1
- package/dist/{backend-http-BvNFrvcY.mjs → backend-http-16SbvnAK.mjs} +2 -2
- package/dist/{backend-http-E7YU0qhn.d.mts → backend-http-3PoBYQoo.d.mts} +15 -3
- package/dist/{backend-http-CfLVYqmj.mjs → backend-http-BPaNYLWj.mjs} +4 -5
- 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-ZKYTe3xa.d.mts → backend-n3hajfrD.d.mts} +12 -6
- package/dist/{backend-vercel-D87pIp2k.mjs → backend-vercel-C4Kee2-K.mjs} +260 -30
- package/dist/{backend-vercel-DTrDiSk5.mjs → backend-vercel-Kd8nvW5A.mjs} +3 -3
- package/dist/{backend-vercel-CIRObA0t.d.mts → backend-vercel-qznuDxnN.d.mts} +20 -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-B-9AvYis.d.mts → blob-CqILvdmr.d.mts} +2 -2
- package/dist/blob-DQK5prXT.mjs +182 -0
- package/dist/{chat-provider-DnHibqnM.mjs → chat-provider-COtsU5jB.mjs} +2 -2
- package/dist/{chat-provider-DsNEBQiA.d.mts → chat-provider-D6jVT4wS.d.mts} +5 -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 +5 -5
- package/dist/{client-xLxeDz0f.d.mts → client-CjAatpV4.d.mts} +67 -10
- package/dist/{commands-BFookFtn.mjs → commands-B5DHjZKK.mjs} +54 -10
- 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-Bfe6pcXb.mjs → engine-DkdkXQEy.mjs} +21 -13
- package/dist/{errors-CB4tnZGz.d.mts → errors-87TVtXeb.d.mts} +1 -1
- package/dist/{client-D0Px7KzF.mjs → errors-C_5rB8Iz.mjs} +104 -4
- package/dist/{events-CewD4kw7.mjs → events-qf5XG5VQ.mjs} +2 -2
- package/dist/{exports-CCzw2Vdw.mjs → exports-CG0P8Qi4.mjs} +59 -33
- package/dist/exports-CrGA5_yN.d.mts +5 -0
- package/dist/{external-BV_cUncs.mjs → external-D3p5T57R.mjs} +4 -2
- package/dist/{folds-jS9wlh9_.d.mts → folds-8wRRXjAt.d.mts} +4 -0
- package/dist/{folds-Dyjput8M.mjs → folds-DGYkMW19.mjs} +3 -2
- package/dist/{index-C05it0mC.d.mts → index-BygAXP7t.d.mts} +2 -2
- package/dist/index.d.mts +6 -6
- package/dist/{index.default-DmKxH_E6.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/inspect/engine.js +5 -5
- 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-eci7w-aP.mjs → main-D2s-QBOK.mjs} +76 -26
- package/dist/{middleware-ChKM1zuu.mjs → middleware-DlJQYiwV.mjs} +2 -2
- 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-DMDST9kl.mjs → project-C_2xs57L.mjs} +1 -1
- package/dist/{provider-poIkXapd.mjs → provider-BaVmi4Hi.mjs} +3 -1
- package/dist/{provider-Cdg1qadh.d.mts → provider-Bm46LAdp.d.mts} +9 -2
- package/dist/{redis-tables-DN9PfIDi.mjs → redis-tables-BWJZHUGg.mjs} +41 -25
- 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-BcmSs4qf.mjs → reducers-8jqLewgw.mjs} +5 -5
- 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/{schemas-Be08nyfW.mjs → schemas-DBmUO6pr.mjs} +2 -1
- package/dist/{serve-B09an9DQ.mjs → serve-lPoBDEds.mjs} +7 -6
- package/dist/{server-qgez0Xny-D5mLfqFQ.d.mts → server-B9gu0qi1-DI1WvcVK.d.mts} +10 -4
- package/dist/{server-DTX0prGr.mjs → server-D82a4uMe.mjs} +4 -4
- package/dist/{server-DtFrWxi3-Boa6gdWO.mjs → server-DX7mM6nm-D7N_7bug.mjs} +32 -23
- package/dist/{serving-CvOWEym2.mjs → serving-DKny38LK.mjs} +1 -1
- package/dist/{session-clear-wHd5GCpZ-BnKSxReK.mjs → session-clear-XJYpmUpg-2q8l_FJR.mjs} +14 -2
- package/dist/{sql-U9WTxrwJ.mjs → sql-Bj0fk6dn.mjs} +2 -2
- package/dist/{sqlite-db-Ds4majtT.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 +4 -2
- package/dist/sync-github.mjs +4 -14
- package/dist/sync-kapso.d.mts +3 -2
- package/dist/sync-kapso.mjs +3 -3
- package/dist/sync-linear.d.mts +12 -2
- package/dist/sync-linear.mjs +191 -9
- package/dist/sync-slack.d.mts +41 -5
- package/dist/sync-slack.mjs +70 -11
- package/dist/sync.chat.d.mts +1 -1
- package/dist/sync.chat.mjs +1 -1
- package/dist/sync.d.mts +2 -2
- package/dist/sync.mjs +3 -3
- package/dist/tags-BpFo6q0_.mjs +14 -0
- package/dist/{targets-C-wdR5wP.mjs → targets-Cgjs29T0.mjs} +1 -1
- package/dist/{tokens-BuCU-61R.mjs → tokens-C5_yGRlL.mjs} +2 -2
- package/dist/{types-aCAf6lGU.d.mts → types-shZmGZ-q.d.mts} +2 -2
- package/dist/{watchers-hash-7DJU9b-n.mjs → watchers-hash-B_5b0-ZG.mjs} +50 -19
- package/docs/01-board/04-messages.md +93 -0
- package/docs/01-board/08-middleware.md +5 -2
- package/docs/02-sync/03-slack.md +6 -2
- package/docs/02-sync/07-linear.md +28 -6
- package/docs/04-reference/01-cli.md +27 -12
- 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-D22M68rk.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
|
@@ -120,6 +120,23 @@ const options = {
|
|
|
120
120
|
value: "k",
|
|
121
121
|
description: "remove a label"
|
|
122
122
|
},
|
|
123
|
+
if: {
|
|
124
|
+
type: "string",
|
|
125
|
+
multiple: true,
|
|
126
|
+
value: "k=v",
|
|
127
|
+
description: "only while the message has this label now"
|
|
128
|
+
},
|
|
129
|
+
"if-unset": {
|
|
130
|
+
type: "string",
|
|
131
|
+
multiple: true,
|
|
132
|
+
value: "k",
|
|
133
|
+
description: "only while the message doesn't have this label"
|
|
134
|
+
},
|
|
135
|
+
"if-revision": {
|
|
136
|
+
type: "string",
|
|
137
|
+
value: "N",
|
|
138
|
+
description: "only if it is still at this revision, as read --json shows it"
|
|
139
|
+
},
|
|
123
140
|
channel: {
|
|
124
141
|
type: "string",
|
|
125
142
|
value: "c",
|
|
@@ -243,6 +260,8 @@ const text = (description = "what it says: - reads it from stdin, and it may be
|
|
|
243
260
|
required: false,
|
|
244
261
|
description
|
|
245
262
|
});
|
|
263
|
+
/** What `--after` means on a reply: a precondition, where on a read it's a position. */
|
|
264
|
+
const AFTER = "a reply only while this is the thread's newest message";
|
|
246
265
|
/** What one of the CLI's help pages lists together, in this order. */
|
|
247
266
|
const groups = [
|
|
248
267
|
"you",
|
|
@@ -369,7 +388,8 @@ const commands = [
|
|
|
369
388
|
option("attach"),
|
|
370
389
|
option("sync"),
|
|
371
390
|
option("no-sync"),
|
|
372
|
-
option("idempotency-key")
|
|
391
|
+
option("idempotency-key"),
|
|
392
|
+
option("after", AFTER)
|
|
373
393
|
],
|
|
374
394
|
examples: [{
|
|
375
395
|
name: "start a thread",
|
|
@@ -387,11 +407,15 @@ const commands = [
|
|
|
387
407
|
options: [
|
|
388
408
|
option("attach"),
|
|
389
409
|
option("idempotency-key"),
|
|
390
|
-
option("no-sync")
|
|
410
|
+
option("no-sync"),
|
|
411
|
+
option("after", AFTER)
|
|
391
412
|
],
|
|
392
413
|
examples: [{
|
|
393
414
|
name: "reply in a thread",
|
|
394
415
|
value: "reply 812 \"on it\""
|
|
416
|
+
}, {
|
|
417
|
+
name: "reply only if #815 is still the thread's newest",
|
|
418
|
+
value: "reply 812 \"on it\" --after 815"
|
|
395
419
|
}]
|
|
396
420
|
},
|
|
397
421
|
{
|
|
@@ -465,10 +489,13 @@ const commands = [
|
|
|
465
489
|
required: true,
|
|
466
490
|
description: "its new text: - reads it from stdin"
|
|
467
491
|
}],
|
|
468
|
-
options: [],
|
|
492
|
+
options: [option("if-revision")],
|
|
469
493
|
examples: [{
|
|
470
494
|
name: "fix a typo",
|
|
471
495
|
value: "edit 812 \"deploys are green again\""
|
|
496
|
+
}, {
|
|
497
|
+
name: "only if no one changed it since you read it",
|
|
498
|
+
value: "edit 812 \"step 3 of 5\" --if-revision 820"
|
|
472
499
|
}]
|
|
473
500
|
},
|
|
474
501
|
{
|
|
@@ -477,7 +504,7 @@ const commands = [
|
|
|
477
504
|
aliases: ["rm", "del"],
|
|
478
505
|
description: "delete a message, leaving a tombstone",
|
|
479
506
|
arguments: [id()],
|
|
480
|
-
options: [],
|
|
507
|
+
options: [option("if-revision")],
|
|
481
508
|
examples: [{
|
|
482
509
|
name: "delete a message",
|
|
483
510
|
value: "delete 812"
|
|
@@ -508,11 +535,25 @@ const commands = [
|
|
|
508
535
|
multiple: true,
|
|
509
536
|
description: "a label to set"
|
|
510
537
|
}],
|
|
511
|
-
options: [
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
538
|
+
options: [
|
|
539
|
+
option("unset"),
|
|
540
|
+
option("if"),
|
|
541
|
+
option("if-unset")
|
|
542
|
+
],
|
|
543
|
+
examples: [
|
|
544
|
+
{
|
|
545
|
+
name: "mark a bug fixed",
|
|
546
|
+
value: "label 812 state=fixed --unset wip"
|
|
547
|
+
},
|
|
548
|
+
{
|
|
549
|
+
name: "claim a thread, unless someone has",
|
|
550
|
+
value: "label 812 owner=claude --if-unset owner"
|
|
551
|
+
},
|
|
552
|
+
{
|
|
553
|
+
name: "move it on from open only",
|
|
554
|
+
value: "label 812 state=fixing --if state=open"
|
|
555
|
+
}
|
|
556
|
+
]
|
|
516
557
|
},
|
|
517
558
|
{
|
|
518
559
|
group: "messages",
|
|
@@ -590,10 +631,13 @@ const commands = [
|
|
|
590
631
|
required: true,
|
|
591
632
|
description: "a channel ('#general'), a thread, or a message link"
|
|
592
633
|
}],
|
|
593
|
-
options: [option("limit", "messages to read")],
|
|
634
|
+
options: [option("limit", "messages to read"), option("after", "where the last read left off: the cursor it printed")],
|
|
594
635
|
examples: [{
|
|
595
636
|
name: "read a Slack channel",
|
|
596
637
|
value: "sources slack read '#general'"
|
|
638
|
+
}, {
|
|
639
|
+
name: "read a Linear project's issues",
|
|
640
|
+
value: "sources linear read https://linear.app/acme/project/checkout-a1b2c3"
|
|
597
641
|
}]
|
|
598
642
|
}, {
|
|
599
643
|
name: "join",
|
|
@@ -166,6 +166,23 @@ declare const options: {
|
|
|
166
166
|
readonly value: 'k';
|
|
167
167
|
readonly description: 'remove a label';
|
|
168
168
|
};
|
|
169
|
+
readonly if: {
|
|
170
|
+
readonly type: 'string';
|
|
171
|
+
readonly multiple: true;
|
|
172
|
+
readonly value: 'k=v';
|
|
173
|
+
readonly description: 'only while the message has this label now';
|
|
174
|
+
};
|
|
175
|
+
readonly 'if-unset': {
|
|
176
|
+
readonly type: 'string';
|
|
177
|
+
readonly multiple: true;
|
|
178
|
+
readonly value: 'k';
|
|
179
|
+
readonly description: "only while the message doesn't have this label";
|
|
180
|
+
};
|
|
181
|
+
readonly 'if-revision': {
|
|
182
|
+
readonly type: 'string';
|
|
183
|
+
readonly value: 'N';
|
|
184
|
+
readonly description: 'only if it is still at this revision, as read --json shows it';
|
|
185
|
+
};
|
|
169
186
|
readonly channel: {
|
|
170
187
|
readonly type: 'string';
|
|
171
188
|
readonly value: 'c';
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { u as BoardError } from "./errors-C_5rB8Iz.mjs";
|
|
2
|
+
import { r as compileAccess } from "./access-CQ39gqEV.mjs";
|
|
2
3
|
//#region src/conformance/index.ts
|
|
3
4
|
var Mismatch = class extends Error {};
|
|
4
5
|
const show = (value) => JSON.stringify(value) ?? String(value);
|
|
@@ -41,6 +42,31 @@ const needRoot = (ctx) => {
|
|
|
41
42
|
reply: ctx.reply
|
|
42
43
|
};
|
|
43
44
|
};
|
|
45
|
+
/** The sync key the sync checks register, a platform of their own, so nothing reaches a real one. */
|
|
46
|
+
const SYNC_KEY = "conformance";
|
|
47
|
+
/**
|
|
48
|
+
* Registers a sync on `SYNC_KEY` for a check, and says where to put the token's registration back
|
|
49
|
+
* after: or why it can't, since registering replaces the token's watchers.
|
|
50
|
+
*/
|
|
51
|
+
async function registerSync(ctx) {
|
|
52
|
+
const registered = (await ctx.board.watchers()).find((registration) => registration.owner === ctx.me.principal);
|
|
53
|
+
if (registered && (Object.keys(registered.watch).length > 0 || (registered.sync?.length ?? 0) > 0)) return { skip: "this token has watchers or a sync registered here, which registering would replace" };
|
|
54
|
+
const where = wokenAt(registered);
|
|
55
|
+
try {
|
|
56
|
+
await ctx.board.register({
|
|
57
|
+
url: null,
|
|
58
|
+
watch: {},
|
|
59
|
+
sync: [{
|
|
60
|
+
key: SYNC_KEY,
|
|
61
|
+
platform: SYNC_KEY
|
|
62
|
+
}]
|
|
63
|
+
});
|
|
64
|
+
} catch (error) {
|
|
65
|
+
if (error instanceof BoardError && error.status === 403) return { skip: "this token can't register a sync here" };
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
return { where };
|
|
69
|
+
}
|
|
44
70
|
async function post(ctx, text, extra = {}) {
|
|
45
71
|
const message = await ctx.board.post(ctx.channel, {
|
|
46
72
|
text,
|
|
@@ -51,6 +77,15 @@ async function post(ctx, text, extra = {}) {
|
|
|
51
77
|
ctx.posted.push(message.id);
|
|
52
78
|
return message;
|
|
53
79
|
}
|
|
80
|
+
async function reply(ctx, re, text, extra = {}) {
|
|
81
|
+
const message = await ctx.board.reply(re, {
|
|
82
|
+
text,
|
|
83
|
+
noSync: true,
|
|
84
|
+
...extra
|
|
85
|
+
});
|
|
86
|
+
ctx.posted.push(message.id);
|
|
87
|
+
return message;
|
|
88
|
+
}
|
|
54
89
|
const CHECKS = [
|
|
55
90
|
{
|
|
56
91
|
group: "core",
|
|
@@ -80,6 +115,31 @@ const CHECKS = [
|
|
|
80
115
|
check((await ctx.board.thread(root.id)).replies.some((m) => m.id === reply.id), `the thread's replies should include ${reply.id}`);
|
|
81
116
|
}
|
|
82
117
|
},
|
|
118
|
+
{
|
|
119
|
+
group: "core",
|
|
120
|
+
name: "replies after a message only while it's the thread's newest, one of any that race",
|
|
121
|
+
run: async (ctx) => {
|
|
122
|
+
const root = await post(ctx, "conformance: a thread to reply after");
|
|
123
|
+
const first = await reply(ctx, root.id, "conformance: after the root", { after: root.id });
|
|
124
|
+
await rejects(reply(ctx, root.id, "conformance: late", { after: root.id }), 409, "newer_message", "a reply after the root, once another came after it");
|
|
125
|
+
same((await ctx.board.thread(root.id)).replies.length, 1, "the replies after a refused one");
|
|
126
|
+
const deleted = await reply(ctx, root.id, "conformance: deleted", { after: first.id });
|
|
127
|
+
await ctx.board.delete(deleted.id);
|
|
128
|
+
await rejects(reply(ctx, root.id, "conformance: after a deleted one", { after: first.id }), 409, "newer_message", "a reply after a message a deleted one came after");
|
|
129
|
+
const racing = await Promise.allSettled([
|
|
130
|
+
1,
|
|
131
|
+
2,
|
|
132
|
+
3
|
|
133
|
+
].map((n) => reply(ctx, root.id, `conformance: racing ${n}`, { after: deleted.id })));
|
|
134
|
+
same(racing.filter((result) => result.status === "fulfilled").length, 1, "the replies that landed of three racing after the same message");
|
|
135
|
+
for (const result of racing) {
|
|
136
|
+
if (result.status === "fulfilled") continue;
|
|
137
|
+
const error = result.reason;
|
|
138
|
+
check(error instanceof BoardError && error.status === 409 && error.code === "newer_message", `a reply that lost the race should be 409 newer_message, got ${error instanceof BoardError ? `${error.status} ${error.code}` : String(error)}`);
|
|
139
|
+
}
|
|
140
|
+
same((await ctx.board.thread(root.id)).replies.length, 3, "the replies after the race, a deleted one included");
|
|
141
|
+
}
|
|
142
|
+
},
|
|
83
143
|
{
|
|
84
144
|
group: "core",
|
|
85
145
|
name: "posts once for a client ID, however many times it is sent",
|
|
@@ -116,6 +176,78 @@ const CHECKS = [
|
|
|
116
176
|
same((await version()).labels.state, void 0, "the label unset");
|
|
117
177
|
}
|
|
118
178
|
},
|
|
179
|
+
{
|
|
180
|
+
group: "core",
|
|
181
|
+
name: "labels a message only while its labels are as the write says, one of any that race",
|
|
182
|
+
run: async (ctx) => {
|
|
183
|
+
const message = await post(ctx, "conformance: a message to claim");
|
|
184
|
+
const labels = async () => (await ctx.board.thread(message.id)).message.labels;
|
|
185
|
+
const racing = await Promise.allSettled([
|
|
186
|
+
1,
|
|
187
|
+
2,
|
|
188
|
+
3
|
|
189
|
+
].map((n) => ctx.board.label(message.id, {
|
|
190
|
+
set: { owner: `agent-${n}` },
|
|
191
|
+
if: { owner: null }
|
|
192
|
+
})));
|
|
193
|
+
const landed = racing.flatMap((result, n) => result.status === "fulfilled" ? [`agent-${n + 1}`] : []);
|
|
194
|
+
same(landed.length, 1, "the claims that landed of three racing where no one had one");
|
|
195
|
+
for (const result of racing) {
|
|
196
|
+
if (result.status === "fulfilled") continue;
|
|
197
|
+
const error = result.reason;
|
|
198
|
+
check(error instanceof BoardError && error.status === 409 && error.code === "label_conflict", `a claim that lost the race should be 409 label_conflict, got ${error instanceof BoardError ? `${error.status} ${error.code}` : String(error)}`);
|
|
199
|
+
}
|
|
200
|
+
same((await labels()).owner, landed[0], "the owner, after the race");
|
|
201
|
+
await rejects(ctx.board.label(message.id, {
|
|
202
|
+
set: { owner: "someone" },
|
|
203
|
+
if: { owner: "no-one" }
|
|
204
|
+
}), 409, "label_conflict", "a label write from a value the label doesn't have");
|
|
205
|
+
same((await labels()).owner, landed[0], "the owner, after a write from the wrong value");
|
|
206
|
+
await ctx.board.label(message.id, {
|
|
207
|
+
unset: ["owner"],
|
|
208
|
+
if: { owner: landed[0] }
|
|
209
|
+
});
|
|
210
|
+
same((await labels()).owner, void 0, "the owner, released from the value it had");
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
group: "core",
|
|
215
|
+
name: "edits and deletes a message only at the revision it was read at, which only edits and deletes change",
|
|
216
|
+
run: async (ctx) => {
|
|
217
|
+
const message = await post(ctx, "conformance: a message to edit");
|
|
218
|
+
const read = async () => (await ctx.board.thread(message.id)).message;
|
|
219
|
+
const first = (await read()).revision;
|
|
220
|
+
check(typeof first === "number", `a message's revision should be a number, got ${show(first)}`);
|
|
221
|
+
await ctx.board.react(message.id, "eyes");
|
|
222
|
+
await ctx.board.label(message.id, { set: { state: "open" } });
|
|
223
|
+
same((await read()).revision, first, "the revision after a reaction and a label");
|
|
224
|
+
await ctx.board.edit(message.id, "conformance: edited", { revision: first });
|
|
225
|
+
const second = (await read()).revision;
|
|
226
|
+
check(second !== first, `an edit should change the revision from ${first}`);
|
|
227
|
+
await rejects(ctx.board.edit(message.id, "conformance: stale", { revision: first }), 409, "revision_conflict", "an edit at a revision an edit came after");
|
|
228
|
+
await rejects(ctx.board.delete(message.id, { revision: first }), 409, "revision_conflict", "a delete at a revision an edit came after");
|
|
229
|
+
const after = await read();
|
|
230
|
+
check(after.text === "conformance: edited" && !after.deleted, `refused writes should leave it as it was, got ${show({
|
|
231
|
+
text: after.text,
|
|
232
|
+
deleted: after.deleted
|
|
233
|
+
})}`);
|
|
234
|
+
const racing = await Promise.allSettled([
|
|
235
|
+
1,
|
|
236
|
+
2,
|
|
237
|
+
3
|
|
238
|
+
].map((n) => ctx.board.edit(message.id, `conformance: racing ${n}`, { revision: second })));
|
|
239
|
+
same(racing.filter((result) => result.status === "fulfilled").length, 1, "the edits that landed of three racing at the same revision");
|
|
240
|
+
for (const result of racing) {
|
|
241
|
+
if (result.status === "fulfilled") continue;
|
|
242
|
+
const error = result.reason;
|
|
243
|
+
check(error instanceof BoardError && error.status === 409 && error.code === "revision_conflict", `an edit that lost the race should be 409 revision_conflict, got ${error instanceof BoardError ? `${error.status} ${error.code}` : String(error)}`);
|
|
244
|
+
}
|
|
245
|
+
const last = (await read()).revision;
|
|
246
|
+
await ctx.board.delete(message.id, { revision: last });
|
|
247
|
+
check((await read()).deleted, "a delete at the revision it has should delete it");
|
|
248
|
+
await rejects(ctx.board.edit(message.id, "conformance: too late", { revision: last }), 409, "revision_conflict", "an edit at a revision a delete came after");
|
|
249
|
+
}
|
|
250
|
+
},
|
|
119
251
|
{
|
|
120
252
|
group: "core",
|
|
121
253
|
name: "matches a label on the message or its thread's root",
|
|
@@ -261,6 +393,24 @@ const CHECKS = [
|
|
|
261
393
|
same(canceled.state, "canceled", "the canceled schedule");
|
|
262
394
|
}
|
|
263
395
|
},
|
|
396
|
+
{
|
|
397
|
+
group: "schedules",
|
|
398
|
+
name: "keeps noSync on a schedule, so its sends stay off every platform",
|
|
399
|
+
run: async (ctx) => {
|
|
400
|
+
const { root } = needRoot(ctx);
|
|
401
|
+
const schedule = await ctx.board.schedule({ re: root.id }, {
|
|
402
|
+
text: "conformance: later, on the board only",
|
|
403
|
+
noSync: true,
|
|
404
|
+
in: "1d"
|
|
405
|
+
});
|
|
406
|
+
try {
|
|
407
|
+
same(schedule.noSync, true, "the schedule's noSync");
|
|
408
|
+
same((await ctx.board.schedules({ re: root.id })).find((s) => s.id === schedule.id)?.noSync, true, "the listed schedule's noSync");
|
|
409
|
+
} finally {
|
|
410
|
+
await ctx.board.cancelSchedule(schedule.id).catch(() => {});
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
},
|
|
264
414
|
{
|
|
265
415
|
group: "attachments",
|
|
266
416
|
name: "takes a file, posts it, and gives it back",
|
|
@@ -341,7 +491,7 @@ const CHECKS = [
|
|
|
341
491
|
group: "sync",
|
|
342
492
|
name: "keeps sync's records: an import as a platform author, its lookups by platform ID, a receipt, and where it's synced",
|
|
343
493
|
run: async (ctx) => {
|
|
344
|
-
const key =
|
|
494
|
+
const key = SYNC_KEY;
|
|
345
495
|
const thread = `${key}:${ctx.labels.conformance}:1`;
|
|
346
496
|
const author = {
|
|
347
497
|
id: `${key}:user:${ctx.labels.conformance}`,
|
|
@@ -354,22 +504,9 @@ const CHECKS = [
|
|
|
354
504
|
thread,
|
|
355
505
|
message: "1"
|
|
356
506
|
};
|
|
357
|
-
const registered =
|
|
358
|
-
if (
|
|
359
|
-
const where =
|
|
360
|
-
try {
|
|
361
|
-
await ctx.board.register({
|
|
362
|
-
url: null,
|
|
363
|
-
watch: {},
|
|
364
|
-
sync: [{
|
|
365
|
-
key,
|
|
366
|
-
platform: key
|
|
367
|
-
}]
|
|
368
|
-
});
|
|
369
|
-
} catch (error) {
|
|
370
|
-
if (error instanceof BoardError && error.status === 403) return "this token can't register a sync here";
|
|
371
|
-
throw error;
|
|
372
|
-
}
|
|
507
|
+
const registered = await registerSync(ctx);
|
|
508
|
+
if ("skip" in registered) return registered.skip;
|
|
509
|
+
const { where } = registered;
|
|
373
510
|
let imported;
|
|
374
511
|
try {
|
|
375
512
|
imported = await ctx.board.syncImport({
|
|
@@ -477,6 +614,109 @@ const CHECKS = [
|
|
|
477
614
|
}).catch(() => {});
|
|
478
615
|
}
|
|
479
616
|
}
|
|
617
|
+
},
|
|
618
|
+
{
|
|
619
|
+
group: "sync",
|
|
620
|
+
name: "queues a platform thread's events in its sync's inbox once, and leases each after the one before is acked",
|
|
621
|
+
run: async (ctx) => {
|
|
622
|
+
const registered = await registerSync(ctx);
|
|
623
|
+
if ("skip" in registered) return registered.skip;
|
|
624
|
+
try {
|
|
625
|
+
const run = ctx.labels.conformance;
|
|
626
|
+
const author = {
|
|
627
|
+
id: `${SYNC_KEY}:user:${run}`,
|
|
628
|
+
handle: "conformance",
|
|
629
|
+
name: "Conformance",
|
|
630
|
+
avatar: null,
|
|
631
|
+
isBot: false
|
|
632
|
+
};
|
|
633
|
+
const thread = `${SYNC_KEY}:${run}:inbox`;
|
|
634
|
+
const eventOf = (message) => ({
|
|
635
|
+
id: `conformance-inbox-${run}-${message}`,
|
|
636
|
+
event: {
|
|
637
|
+
kind: "message",
|
|
638
|
+
channel: `${SYNC_KEY}:${run}`,
|
|
639
|
+
thread,
|
|
640
|
+
subthread: null,
|
|
641
|
+
message,
|
|
642
|
+
isRoot: message === "1",
|
|
643
|
+
text: `conformance: inbox event ${message}`,
|
|
644
|
+
context: null,
|
|
645
|
+
author
|
|
646
|
+
},
|
|
647
|
+
raw: {
|
|
648
|
+
conformance: run,
|
|
649
|
+
message
|
|
650
|
+
}
|
|
651
|
+
});
|
|
652
|
+
const pushed = [eventOf("1"), eventOf("2")];
|
|
653
|
+
await ctx.board.inboxPush(SYNC_KEY, pushed);
|
|
654
|
+
await ctx.board.inboxPush(SYNC_KEY, [pushed[0]]);
|
|
655
|
+
const lease = async () => (await ctx.board.inboxNext(SYNC_KEY, { limit: 50 })).filter((item) => item.thread === thread);
|
|
656
|
+
for (const { event, raw } of pushed) {
|
|
657
|
+
const leased = await lease();
|
|
658
|
+
same(leased.length, 1, `the events leased from the platform thread, for message ${event.message}`);
|
|
659
|
+
const item = leased[0];
|
|
660
|
+
same(item.event.kind === "message" ? item.event.message : null, event.message, "the platform message leased next");
|
|
661
|
+
same(JSON.stringify(item.raw), JSON.stringify(raw), "the leased event's raw payload");
|
|
662
|
+
await ctx.board.inboxAck(SYNC_KEY, item.id, { lease: item.lease });
|
|
663
|
+
}
|
|
664
|
+
same((await lease()).length, 0, "the events leased once both are acked");
|
|
665
|
+
} finally {
|
|
666
|
+
await ctx.board.register({
|
|
667
|
+
...registered.where,
|
|
668
|
+
watch: {}
|
|
669
|
+
}).catch(() => {});
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
},
|
|
673
|
+
{
|
|
674
|
+
group: "sync",
|
|
675
|
+
name: "hands a sync each message change as a match of its @sync watcher",
|
|
676
|
+
run: async (ctx) => {
|
|
677
|
+
const access = compileAccess(ctx.me.can);
|
|
678
|
+
if (access.anywhere("sync") && !access.can("sync", ctx.channel)) return `this token can't sync in ${ctx.channel}`;
|
|
679
|
+
const registered = await registerSync(ctx);
|
|
680
|
+
if ("skip" in registered) return registered.skip;
|
|
681
|
+
try {
|
|
682
|
+
const message = await post(ctx, "conformance: for a sync");
|
|
683
|
+
const deadline = Date.now() + 1e4;
|
|
684
|
+
let match;
|
|
685
|
+
while (!match && Date.now() < deadline) match = (await ctx.board.matches({ wait: 5 })).find((m) => m.watcher === "@sync" && m.event.message?.id === message.id);
|
|
686
|
+
check(match, "a pull should hand the sync a match for the post, as its @sync watcher's");
|
|
687
|
+
same(match.event.type, "message.posted", "the sync's match");
|
|
688
|
+
await ctx.board.ack(match.id, { lease: match.lease });
|
|
689
|
+
} finally {
|
|
690
|
+
await ctx.board.register({
|
|
691
|
+
...registered.where,
|
|
692
|
+
watch: {}
|
|
693
|
+
}).catch(() => {});
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
},
|
|
697
|
+
{
|
|
698
|
+
group: "sync",
|
|
699
|
+
name: "keeps noSync on a post and a reply, so sync sends neither",
|
|
700
|
+
run: async (ctx) => {
|
|
701
|
+
const { root, reply } = needRoot(ctx);
|
|
702
|
+
const thread = await ctx.board.thread(root.id);
|
|
703
|
+
same(thread.message.noSync, true, "a post's noSync, read back");
|
|
704
|
+
same(thread.replies.find((m) => m.id === reply.id)?.noSync, true, "a reply's noSync, read back");
|
|
705
|
+
}
|
|
706
|
+
},
|
|
707
|
+
{
|
|
708
|
+
group: "sync",
|
|
709
|
+
name: "finds no one for a platform account nobody linked",
|
|
710
|
+
run: async (ctx) => {
|
|
711
|
+
let found;
|
|
712
|
+
try {
|
|
713
|
+
found = await ctx.board.syncFind({ account: `${SYNC_KEY}:user:${ctx.labels.conformance}` });
|
|
714
|
+
} catch (error) {
|
|
715
|
+
if (error instanceof BoardError && error.status === 403) return "this token can't look up a sync here";
|
|
716
|
+
throw error;
|
|
717
|
+
}
|
|
718
|
+
same(found.person, null, "who an account nobody linked is");
|
|
719
|
+
}
|
|
480
720
|
}
|
|
481
721
|
];
|
|
482
722
|
/** Runs the checks against `board`, in order, and reports each. It never throws for what the board answers. */
|
package/dist/conformance.d.mts
CHANGED
package/dist/conformance.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as conformance } from "./conformance-
|
|
1
|
+
import { t as conformance } from "./conformance-C7fwIvmN.mjs";
|
|
2
2
|
export { conformance };
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { a as typeOf, i as fileSize, r as IMPORT_BYTES_MAX, t as ATTACHMENT_BYTES_MAX } from "./files-SniSmeEr.mjs";
|
|
2
|
-
import { C as toStorable, c as ChannelPath, m as MESSAGE_TEXT_MAX } from "./schemas-
|
|
3
|
-
import { c as externalMessageKey, l as isThreadTarget, s as channelOf } from "./external-
|
|
2
|
+
import { C as toStorable, c as ChannelPath, m as MESSAGE_TEXT_MAX } from "./schemas-DBmUO6pr.mjs";
|
|
3
|
+
import { c as externalMessageKey, l as isThreadTarget, s as channelOf } from "./external-D3p5T57R.mjs";
|
|
4
4
|
import { t as canonicalEmoji } from "./emoji-CTLpvH-A.mjs";
|
|
5
|
+
import { r as badRequest, s as notFound, t as HttpError } from "./errors-C_5rB8Iz.mjs";
|
|
5
6
|
import { r as derivedId, t as waitUntil } from "./wait-until-Dw4XrIDQ.mjs";
|
|
6
|
-
import { r as badRequest, s as notFound, t as HttpError } from "./errors-CusN4x2l.mjs";
|
|
7
7
|
import { n as replaceMentions } from "./mentions-CB6ShY4R.mjs";
|
|
8
|
-
import { t as DroppedError } from "./middleware-
|
|
8
|
+
import { t as DroppedError } from "./middleware-DlJQYiwV.mjs";
|
|
9
9
|
import { t as afterRetry } from "./attempts-HXcskN1w.mjs";
|
|
10
|
-
import { a as channelMissing, i as UnreadableError, n as ResolvedTarget, o as routesFor, t as ReceivedEvent } from "./provider-
|
|
11
|
-
import { a as immutable, n as SyncOverrides } from "./events-
|
|
10
|
+
import { a as channelMissing, i as UnreadableError, n as ResolvedTarget, o as routesFor, t as ReceivedEvent } from "./provider-BaVmi4Hi.mjs";
|
|
11
|
+
import { a as immutable, n as SyncOverrides } from "./events-qf5XG5VQ.mjs";
|
|
12
12
|
import { t as displayText } from "./bots-CGkO76qH.mjs";
|
|
13
13
|
//#region src/sync/engine.ts
|
|
14
14
|
/** `files`: a message with files may have no text. */
|
|
@@ -872,7 +872,7 @@ function createSyncEngine(entries) {
|
|
|
872
872
|
const root = await messageOf(rootId);
|
|
873
873
|
const createdHere = root?.origin?.message === `lifecycle:${meta.id}`;
|
|
874
874
|
const desired = labels((createdHere ? rootDraft({
|
|
875
|
-
text: event
|
|
875
|
+
text: lifecycleText(event),
|
|
876
876
|
labels: event.labels,
|
|
877
877
|
author: event.author,
|
|
878
878
|
raw
|
|
@@ -883,7 +883,7 @@ function createSyncEngine(entries) {
|
|
|
883
883
|
if (root && !root.deleted && (Object.keys(set).length || unset.length)) await setLabels(event.author, set, unset);
|
|
884
884
|
if (event.retitled && root && !root.deleted && root.from === key && root.origin?.thread === event.thread) {
|
|
885
885
|
const text = clampText(overrides.text ?? rootDraft({
|
|
886
|
-
text: event
|
|
886
|
+
text: lifecycleText(event),
|
|
887
887
|
labels: root.labels,
|
|
888
888
|
author: event.author,
|
|
889
889
|
raw
|
|
@@ -916,7 +916,7 @@ function createSyncEngine(entries) {
|
|
|
916
916
|
message: null
|
|
917
917
|
};
|
|
918
918
|
const draft = rootDraft({
|
|
919
|
-
text: event
|
|
919
|
+
text: lifecycleText(event),
|
|
920
920
|
labels: event.labels,
|
|
921
921
|
author: event.author,
|
|
922
922
|
raw
|
|
@@ -1289,14 +1289,14 @@ function createSyncEngine(entries) {
|
|
|
1289
1289
|
postedAsPerson = true;
|
|
1290
1290
|
return asPerson;
|
|
1291
1291
|
}
|
|
1292
|
-
return platform.post(native(target), "channel", body);
|
|
1292
|
+
if (root.attachments.length === 0 || !platform.startsThreadsWithFiles) return platform.post(native(target), "channel", body);
|
|
1293
1293
|
}
|
|
1294
1294
|
try {
|
|
1295
1295
|
const sent = await platform.post(native(target), "channel", {
|
|
1296
1296
|
...body,
|
|
1297
1297
|
files: await messageFiles(root)
|
|
1298
1298
|
});
|
|
1299
|
-
uploaded = sent.files ?? [];
|
|
1299
|
+
uploaded = mediaOnly ? sent.files ?? [] : sent.files;
|
|
1300
1300
|
return sent;
|
|
1301
1301
|
} catch (error) {
|
|
1302
1302
|
if (!platform.refusesFiles(error) && !keepsNoFiles(error)) throw error;
|
|
@@ -1500,10 +1500,13 @@ function createSyncEngine(entries) {
|
|
|
1500
1500
|
return 403;
|
|
1501
1501
|
};
|
|
1502
1502
|
/** A platform channel or thread, read for the caller: nothing is synced, stored, or joined. */
|
|
1503
|
-
async function readSource(key, target, limit) {
|
|
1503
|
+
async function readSource(key, target, limit, after) {
|
|
1504
1504
|
const platform = platformOf(key);
|
|
1505
1505
|
try {
|
|
1506
|
-
return await platform.read(target, {
|
|
1506
|
+
return await platform.read(target, {
|
|
1507
|
+
limit,
|
|
1508
|
+
after
|
|
1509
|
+
});
|
|
1507
1510
|
} catch (error) {
|
|
1508
1511
|
if (error instanceof UnreadableError) throw new HttpError(unreadableStatus(error.code), error.code, error.message);
|
|
1509
1512
|
throw badRequest(`can't read ${target} from ${key}: ${describe(error)}`, "platform_error");
|
|
@@ -1542,5 +1545,10 @@ function createSyncEngine(entries) {
|
|
|
1542
1545
|
joinSource
|
|
1543
1546
|
};
|
|
1544
1547
|
}
|
|
1548
|
+
/** A lifecycle root's text: the title, its link, and what the PR or issue says. */
|
|
1549
|
+
function lifecycleText(event) {
|
|
1550
|
+
const head = event.url ? `${event.title}\n${event.url}` : event.title;
|
|
1551
|
+
return event.body ? `${head}\n\n${event.body}` : head;
|
|
1552
|
+
}
|
|
1545
1553
|
//#endregion
|
|
1546
1554
|
export { createSyncEngine };
|