slack-term 1.31.3 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slack-term",
3
- "version": "1.31.3",
3
+ "version": "1.31.5",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/snomiao/slack-term.git"
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
- try {
2137
- await reactionAdd(token, channelId, ts, ASK_MARKER, cookie);
2138
- } catch (e: unknown) {
2139
- // Cosmetic + searchability, not correctness: the body still identifies it.
2140
- console.error(` (marker ${ASK_MARKER} を付けられませんでした: ${e instanceof Error ? e.message : String(e)})`);
2141
- }
2142
-
2143
- // Seeded one at a time and in order: Slack keeps reactions in the order they
2144
- // were added, so sequential adds are what make the pills read 1,2,3 rather
2145
- // than shuffled. A parallel add is not an optimization here, it is a bug.
2146
- for (let i = 0; i < reactable.length; i++) {
2147
- try {
2148
- await reactionAdd(token, channelId, ts, ASK_KEYCAPS[i]!.name, cookie);
2149
- } catch (e: unknown) {
2150
- // A missing seed only costs the answerer a trip to the emoji picker, so
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
- try {
2457
- await reactionAdd(token, channelId, ts, POLL_MARKER, cookie);
2458
- } catch (e: unknown) {
2459
- console.error(` (marker ${POLL_MARKER} を付けられませんでした: ${e instanceof Error ? e.message : String(e)})`);
2460
- }
2461
-
2462
- // Sequential and in order: Slack keeps reactions in the order they were added,
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(` (リアクション ${POLL_KEYCAPS[i]!.name} を付けられませんでした: ${e instanceof Error ? e.message : String(e)})`);
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
+ }