slack-term 1.31.4 → 1.31.5
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/cli.js +96 -96
- package/package.json +1 -1
- package/ts/cli.ts +27 -35
- package/ts/reactionSeed.ts +82 -0
package/package.json
CHANGED
package/ts/cli.ts
CHANGED
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
pollParseMessage,
|
|
39
39
|
pollTally,
|
|
40
40
|
} from "./poll.ts";
|
|
41
|
+
import { seedReactionsInOrder } from "./reactionSeed.ts";
|
|
41
42
|
import {
|
|
42
43
|
authTest,
|
|
43
44
|
authScopes,
|
|
@@ -2133,25 +2134,21 @@ async function cmdAsk(token: string, args: AskArgs): Promise<void> {
|
|
|
2133
2134
|
// NOTE the colons: the modifier's argument is itself colon-wrapped, and the
|
|
2134
2135
|
// bare `has:question` form does NOT error — it silently degrades to a
|
|
2135
2136
|
// full-text search and returns plausible-looking counts. See ts/todo.ts.
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
}
|
|
2150
|
-
|
|
2151
|
-
// warn and keep going rather than abandon a question already posted.
|
|
2152
|
-
console.error(` (リアクション ${ASK_KEYCAPS[i]!.name} を付けられませんでした: ${e instanceof Error ? e.message : String(e)})`);
|
|
2153
|
-
}
|
|
2154
|
-
}
|
|
2137
|
+
// Marker first, then 1,2,3 — and spaced out. Sequential awaits alone are NOT
|
|
2138
|
+
// enough: measured on two real questions from the same build, one came back
|
|
2139
|
+
// `two|one|question|three`. See ts/reactionSeed.ts for the evidence and for
|
|
2140
|
+
// SLACK_REACTION_SEED_GAP_MS.
|
|
2141
|
+
await seedReactionsInOrder(
|
|
2142
|
+
[ASK_MARKER, ...reactable.map((_, i) => ASK_KEYCAPS[i]!.name)],
|
|
2143
|
+
(name) => reactionAdd(token, channelId, ts, name, cookie),
|
|
2144
|
+
(name, e) => {
|
|
2145
|
+
const why = e instanceof Error ? e.message : String(e);
|
|
2146
|
+
// The marker is cosmetic + searchability; a keycap only costs the
|
|
2147
|
+
// answerer a trip to the emoji picker. Neither abandons a posted question.
|
|
2148
|
+
if (name === ASK_MARKER) console.error(` (marker ${name} を付けられませんでした: ${why})`);
|
|
2149
|
+
else console.error(` (リアクション ${name} を付けられませんでした: ${why})`);
|
|
2150
|
+
},
|
|
2151
|
+
);
|
|
2155
2152
|
|
|
2156
2153
|
let permalink = "";
|
|
2157
2154
|
try {
|
|
@@ -2453,24 +2450,19 @@ async function cmdPoll(token: string, args: PollArgs): Promise<void> {
|
|
|
2453
2450
|
// `has::ballot_box_with_ballot:` list every poll. An unrelated reaction on the message is
|
|
2454
2451
|
// already ignored by the tally (it counts keycap names only), so this costs
|
|
2455
2452
|
// the ballot nothing.
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
// so this is what makes the pills read 1,2,3 rather than shuffled. Parallel
|
|
2464
|
-
// adds are not an optimization here, they are a bug.
|
|
2465
|
-
for (let i = 0; i < choices.length; i++) {
|
|
2466
|
-
try {
|
|
2467
|
-
await reactionAdd(token, channelId, ts, POLL_KEYCAPS[i]!.name, cookie);
|
|
2468
|
-
} catch (e: unknown) {
|
|
2453
|
+
// Marker first, then 1,2,3 — and spaced out, for the reason `ask` documents
|
|
2454
|
+
// at its own seeding call and ts/reactionSeed.ts documents in full.
|
|
2455
|
+
await seedReactionsInOrder(
|
|
2456
|
+
[POLL_MARKER, ...choices.map((_, i) => POLL_KEYCAPS[i]!.name)],
|
|
2457
|
+
(name) => reactionAdd(token, channelId, ts, name, cookie),
|
|
2458
|
+
(name, e) => {
|
|
2459
|
+
const why = e instanceof Error ? e.message : String(e);
|
|
2469
2460
|
// A missing seed costs a voter a trip to the emoji picker; it does not
|
|
2470
2461
|
// invalidate a poll that is already posted.
|
|
2471
|
-
console.error(` (
|
|
2472
|
-
|
|
2473
|
-
|
|
2462
|
+
if (name === POLL_MARKER) console.error(` (marker ${name} を付けられませんでした: ${why})`);
|
|
2463
|
+
else console.error(` (リアクション ${name} を付けられませんでした: ${why})`);
|
|
2464
|
+
},
|
|
2465
|
+
);
|
|
2474
2466
|
|
|
2475
2467
|
let permalink = "";
|
|
2476
2468
|
try {
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// Seeding the answer pills on an `ask` / `poll` message, in order.
|
|
2
|
+
//
|
|
3
|
+
// MEASURED, 2026-08-31, two real questions posted five seconds apart by the
|
|
4
|
+
// SAME build:
|
|
5
|
+
//
|
|
6
|
+
// 07:42:31 two | one | question | three ← shuffled
|
|
7
|
+
// 07:42:36 question | one | two | three | four ← correct
|
|
8
|
+
//
|
|
9
|
+
// The adds have been sequential (`await` inside a `for`) since the first commit
|
|
10
|
+
// of `ask`, and there has never been a `Promise.all` here — so the reordering
|
|
11
|
+
// happens on Slack's side, not ours. The comments that used to sit at both call
|
|
12
|
+
// sites ("Slack keeps reactions in the order they were added, so sequential
|
|
13
|
+
// adds are what make the pills read 1,2,3") asserted a guarantee that does not
|
|
14
|
+
// exist, and the shuffled message above is the counter-example.
|
|
15
|
+
//
|
|
16
|
+
// The working theory is that the display order comes from a reaction timestamp
|
|
17
|
+
// stored at second resolution: adds landing inside one second are tie-broken
|
|
18
|
+
// arbitrarily, while adds that straddle a boundary keep their order. That fits
|
|
19
|
+
// both samples — the slower message came out right — and it is why the gap
|
|
20
|
+
// below is just over a second rather than a token 100ms.
|
|
21
|
+
//
|
|
22
|
+
// It is a THEORY. Confirming it needs `reactions.add` against a real workspace,
|
|
23
|
+
// which this repo's QA rule forbids (see CLAUDE.md: read-only only), and Slack
|
|
24
|
+
// exposes no timestamp on a reaction to measure it from. So the gap is tunable
|
|
25
|
+
// without a code change: set SLACK_REACTION_SEED_GAP_MS, and 0 restores the old
|
|
26
|
+
// back-to-back behaviour.
|
|
27
|
+
|
|
28
|
+
/** Just over one second, so two consecutive seeds cannot share a second. */
|
|
29
|
+
export const DEFAULT_SEED_GAP_MS = 1100;
|
|
30
|
+
|
|
31
|
+
/** True under vitest or `bun test`. Same check as ts/todo.ts, and for the same
|
|
32
|
+
* reason: a seam that only tests replace gets forgotten, and then the suite
|
|
33
|
+
* waits real seconds per seeded reaction.
|
|
34
|
+
*
|
|
35
|
+
* It takes `env` so it can be tested by VALUE. Testing it by deleting
|
|
36
|
+
* NODE_ENV/VITEST from `process.env` and putting them back is what a first
|
|
37
|
+
* version did, and under `bun test --parallel=2` the deletion was visible to
|
|
38
|
+
* every CLI subprocess another test file spawned in that window: the suite
|
|
39
|
+
* went from 45s to 169s because those children then waited the real gap. */
|
|
40
|
+
export function isTestEnv(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
41
|
+
return !!env.VITEST || env.NODE_ENV === "test" || !!env.BUN_TEST;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const _internals = {
|
|
45
|
+
isTestEnv: (): boolean => isTestEnv(),
|
|
46
|
+
sleep: (ms: number): Promise<void> =>
|
|
47
|
+
_internals.isTestEnv() ? Promise.resolve() : new Promise((res) => setTimeout(res, ms)),
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/** The configured gap. Invalid values fall back to the default rather than
|
|
51
|
+
* disabling the gap silently — a typo must not quietly restore the bug. */
|
|
52
|
+
export function seedGapMs(env: NodeJS.ProcessEnv = process.env): number {
|
|
53
|
+
const raw = env.SLACK_REACTION_SEED_GAP_MS;
|
|
54
|
+
if (raw === undefined || raw === "") return DEFAULT_SEED_GAP_MS;
|
|
55
|
+
const n = Number(raw);
|
|
56
|
+
if (!Number.isFinite(n) || n < 0) return DEFAULT_SEED_GAP_MS;
|
|
57
|
+
return n;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Add `names` one at a time, in order, pausing between them.
|
|
61
|
+
*
|
|
62
|
+
* A failed seed is reported and skipped, never fatal: the message is already
|
|
63
|
+
* posted, and a missing pill costs the answerer a trip to the emoji picker.
|
|
64
|
+
* The pause is skipped after the last add, and after one that failed — there
|
|
65
|
+
* is nothing left to keep in order. */
|
|
66
|
+
export async function seedReactionsInOrder(
|
|
67
|
+
names: readonly string[],
|
|
68
|
+
add: (name: string) => Promise<void>,
|
|
69
|
+
onError: (name: string, e: unknown) => void,
|
|
70
|
+
gapMs: number = seedGapMs(),
|
|
71
|
+
): Promise<void> {
|
|
72
|
+
for (let i = 0; i < names.length; i++) {
|
|
73
|
+
let ok = true;
|
|
74
|
+
try {
|
|
75
|
+
await add(names[i]!);
|
|
76
|
+
} catch (e: unknown) {
|
|
77
|
+
ok = false;
|
|
78
|
+
onError(names[i]!, e);
|
|
79
|
+
}
|
|
80
|
+
if (ok && gapMs > 0 && i < names.length - 1) await _internals.sleep(gapMs);
|
|
81
|
+
}
|
|
82
|
+
}
|