@binaryandthread/cli 0.3.1 → 0.3.2
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/bin.js +24 -16
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-VDOFLQ2P.js → chunk-BFMH7VCE.js} +2 -2
- package/dist/{chunk-ZNDKXHMV.js → chunk-RGFMXWD5.js} +1 -1
- package/dist/chunk-RGFMXWD5.js.map +1 -0
- package/dist/{config-GVEB4R7I.js → config-WV4DTQXU.js} +2 -2
- package/dist/{interactive-JALKLT56.js → interactive-BOS6VMRJ.js} +3 -3
- package/dist/{poll-O3VUD5WV.js → poll-II3TVJLK.js} +2 -2
- package/dist/poll-II3TVJLK.js.map +1 -0
- package/package.json +1 -1
- package/dist/chunk-ZNDKXHMV.js.map +0 -1
- package/dist/poll-O3VUD5WV.js.map +0 -1
- /package/dist/{chunk-VDOFLQ2P.js.map → chunk-BFMH7VCE.js.map} +0 -0
- /package/dist/{config-GVEB4R7I.js.map → config-WV4DTQXU.js.map} +0 -0
- /package/dist/{interactive-JALKLT56.js.map → interactive-BOS6VMRJ.js.map} +0 -0
|
@@ -1378,7 +1378,7 @@ function registerStage(program, makeClient, loadConfig) {
|
|
|
1378
1378
|
brand
|
|
1379
1379
|
});
|
|
1380
1380
|
if (res.save) {
|
|
1381
|
-
const { saveConfig } = await import("./config-
|
|
1381
|
+
const { saveConfig } = await import("./config-WV4DTQXU.js");
|
|
1382
1382
|
saveConfig(res.save);
|
|
1383
1383
|
}
|
|
1384
1384
|
printResult(res, json);
|
|
@@ -1434,4 +1434,4 @@ export {
|
|
|
1434
1434
|
runStage,
|
|
1435
1435
|
registerStage
|
|
1436
1436
|
};
|
|
1437
|
-
//# sourceMappingURL=chunk-
|
|
1437
|
+
//# sourceMappingURL=chunk-BFMH7VCE.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/config.ts"],"sourcesContent":["// Local config — ~/.config/bt/config.json.\n//\n// Holds ONLY non-sensitive fields (SECURITY G15): cart_id, email, region, defaults.\n// The auth TOKEN is NEVER here — it goes in the OS keychain in Wave 5. Persisting a\n// token in plaintext config would be the exact leak G15 forbids.\n//\n// Writes are ATOMIC (F-B1): write to config.json.tmp then fs.renameSync, so a crash\n// mid-write never corrupts the file (rename is atomic on POSIX + Windows same-volume).\n\nimport * as fs from \"node:fs\"\nimport * as os from \"node:os\"\nimport * as path from \"node:path\"\n\nexport interface BtConfig {\n cart_id?: string\n email?: string\n region?: string\n defaultSize?: string\n defaultColour?: string\n defaultBranch?: string\n /** RUNTIME-ONLY (ENG-21): where the effective region came from this run — flag, saved config, or\n * a locale guess. Deliberately NOT in ALLOWED_KEYS, so saveConfig always strips it. */\n region_source?: \"flag\" | \"config\" | \"locale\"\n}\n\n// Persisted keys exclude the RUNTIME-ONLY fields (region_source) — typing ALLOWED_KEYS as the full\n// `keyof BtConfig` wrongly widened `key` to include region_source's literal union, so `out[key] = v`\n// (a string) failed to typecheck. PersistedKey narrows it to the string-valued, persistable fields.\ntype PersistedKey = Exclude<keyof BtConfig, \"region_source\">\n\n// Only these keys are ever persisted — anything else (notably `token`) is dropped (G15).\nconst ALLOWED_KEYS: Array<PersistedKey> = [\n \"cart_id\",\n \"email\",\n \"region\",\n \"defaultSize\",\n \"defaultColour\",\n \"defaultBranch\",\n]\n\n/** The config directory: $XDG_CONFIG_HOME/bt or ~/.config/bt. */\nexport function configDir(): string {\n const xdg = process.env.XDG_CONFIG_HOME\n const base = xdg && xdg.trim() ? xdg : path.join(os.homedir(), \".config\")\n return path.join(base, \"bt\")\n}\n\nexport function configPath(): string {\n return path.join(configDir(), \"config.json\")\n}\n\n/** Load the config, or {} if none / unreadable. Never throws. */\nexport function loadConfig(): BtConfig {\n try {\n const raw = fs.readFileSync(configPath(), \"utf8\")\n const parsed = JSON.parse(raw) as Record<string, unknown>\n return sanitize(parsed)\n } catch {\n return {}\n }\n}\n\n/** Merge-and-save the given fields. Strips any non-allowlisted key (G15). Atomic (F-B1). */\nexport function saveConfig(patch: Partial<BtConfig>): BtConfig {\n const merged = { ...loadConfig(), ...sanitize(patch as Record<string, unknown>) }\n const dir = configDir()\n fs.mkdirSync(dir, { recursive: true })\n const target = configPath()\n const tmp = target + \".tmp\"\n // write to a temp file then atomically rename (F-B1)\n fs.writeFileSync(tmp, JSON.stringify(merged, null, 2), { encoding: \"utf8\", mode: 0o600 })\n fs.renameSync(tmp, target)\n return merged\n}\n\n/** Wipe the local config file (G15 erasure — `bt forget`). Never throws. */\nexport function forgetConfig(): void {\n try {\n fs.rmSync(configPath(), { force: true })\n } catch {\n /* ignore */\n }\n}\n\n/** Keep only allowlisted, non-sensitive keys (drops `token` and anything unexpected). */\nfunction sanitize(obj: Record<string, unknown>): BtConfig {\n const out: BtConfig = {}\n for (const key of ALLOWED_KEYS) {\n const v = obj[key]\n if (typeof v === \"string\" && v.length > 0) {\n out[key] = v\n }\n }\n return out\n}\n"],"mappings":";;;AASA,YAAY,QAAQ;AACpB,YAAY,QAAQ;AACpB,YAAY,UAAU;AAoBtB,IAAM,eAAoC;AAAA,EACxC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAGO,SAAS,YAAoB;AAClC,QAAM,MAAM,QAAQ,IAAI;AACxB,QAAM,OAAO,OAAO,IAAI,KAAK,IAAI,MAAW,UAAQ,WAAQ,GAAG,SAAS;AACxE,SAAY,UAAK,MAAM,IAAI;AAC7B;AAEO,SAAS,aAAqB;AACnC,SAAY,UAAK,UAAU,GAAG,aAAa;AAC7C;AAGO,SAAS,aAAuB;AACrC,MAAI;AACF,UAAM,MAAS,gBAAa,WAAW,GAAG,MAAM;AAChD,UAAM,SAAS,KAAK,MAAM,GAAG;AAC7B,WAAO,SAAS,MAAM;AAAA,EACxB,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;AAGO,SAAS,WAAW,OAAoC;AAC7D,QAAM,SAAS,EAAE,GAAG,WAAW,GAAG,GAAG,SAAS,KAAgC,EAAE;AAChF,QAAM,MAAM,UAAU;AACtB,EAAG,aAAU,KAAK,EAAE,WAAW,KAAK,CAAC;AACrC,QAAM,SAAS,WAAW;AAC1B,QAAM,MAAM,SAAS;AAErB,EAAG,iBAAc,KAAK,KAAK,UAAU,QAAQ,MAAM,CAAC,GAAG,EAAE,UAAU,QAAQ,MAAM,IAAM,CAAC;AACxF,EAAG,cAAW,KAAK,MAAM;AACzB,SAAO;AACT;AAGO,SAAS,eAAqB;AACnC,MAAI;AACF,IAAG,UAAO,WAAW,GAAG,EAAE,OAAO,KAAK,CAAC;AAAA,EACzC,QAAQ;AAAA,EAER;AACF;AAGA,SAAS,SAAS,KAAwC;AACxD,QAAM,MAAgB,CAAC;AACvB,aAAW,OAAO,cAAc;AAC9B,UAAM,IAAI,IAAI,GAAG;AACjB,QAAI,OAAO,MAAM,YAAY,EAAE,SAAS,GAAG;AACzC,UAAI,GAAG,IAAI;AAAA,IACb;AAAA,EACF;AACA,SAAO;AACT;","names":[]}
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
forgetConfig,
|
|
6
6
|
loadConfig,
|
|
7
7
|
saveConfig
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-RGFMXWD5.js";
|
|
9
9
|
import "./chunk-7OCVIDC7.js";
|
|
10
10
|
export {
|
|
11
11
|
configDir,
|
|
@@ -14,4 +14,4 @@ export {
|
|
|
14
14
|
loadConfig,
|
|
15
15
|
saveConfig
|
|
16
16
|
};
|
|
17
|
-
//# sourceMappingURL=config-
|
|
17
|
+
//# sourceMappingURL=config-WV4DTQXU.js.map
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
runStage,
|
|
8
8
|
runView,
|
|
9
9
|
sizesForColour
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-BFMH7VCE.js";
|
|
11
11
|
import "./chunk-7OCVIDC7.js";
|
|
12
12
|
|
|
13
13
|
// src/interactive.ts
|
|
@@ -96,7 +96,7 @@ async function stageInteractive(client, config, product, brand, select, print, s
|
|
|
96
96
|
if (res.save && saveConfig) {
|
|
97
97
|
saveConfig(res.save);
|
|
98
98
|
} else if (res.save) {
|
|
99
|
-
const { saveConfig: realSave } = await import("./config-
|
|
99
|
+
const { saveConfig: realSave } = await import("./config-WV4DTQXU.js");
|
|
100
100
|
realSave(res.save);
|
|
101
101
|
}
|
|
102
102
|
print(res.lines);
|
|
@@ -104,4 +104,4 @@ async function stageInteractive(client, config, product, brand, select, print, s
|
|
|
104
104
|
export {
|
|
105
105
|
runInteractiveBrowse
|
|
106
106
|
};
|
|
107
|
-
//# sourceMappingURL=interactive-
|
|
107
|
+
//# sourceMappingURL=interactive-BOS6VMRJ.js.map
|
|
@@ -19,7 +19,7 @@ async function pollUntilPlaced(client, sessionId, opts = {}) {
|
|
|
19
19
|
opts.onTick?.(attempt);
|
|
20
20
|
const result = await client.poll(sessionId);
|
|
21
21
|
if (result.status === "placed") {
|
|
22
|
-
return { status: "placed", order_id: result.order_id };
|
|
22
|
+
return { status: "placed", order_id: result.order_id, summary: result.summary ?? null };
|
|
23
23
|
}
|
|
24
24
|
if (result.status === "expired") {
|
|
25
25
|
return { status: "expired", order_id: result.order_id };
|
|
@@ -36,4 +36,4 @@ async function pollUntilPlaced(client, sessionId, opts = {}) {
|
|
|
36
36
|
export {
|
|
37
37
|
pollUntilPlaced
|
|
38
38
|
};
|
|
39
|
-
//# sourceMappingURL=poll-
|
|
39
|
+
//# sourceMappingURL=poll-II3TVJLK.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/poll.ts"],"sourcesContent":["// Bounded poll loop — the checkout-completion watcher (F-C5 / F-I3).\n//\n// After the browser hop, the CLI polls the bt_session until the order is placed. This loop\n// is the single most safety-critical control flow in the write path that runs unattended:\n// it MUST be bounded so a closed-browser / never-returns buyer can never hang the terminal\n// forever (F-I3). It polls every `intervalMs` (5s), stops the instant the session is\n// \"placed\" or \"expired\", and stops at a `deadlineMs` (20-min cap) returning \"timeout\".\n//\n// The clock + sleep are INJECTED (defaults wired to the real ones) so the deadline is\n// deterministically testable without burning 20 minutes of wall-clock — and so nothing\n// authoritative is ever computed from an un-injectable clock (F-I5).\n\nimport type { PollResult, SettledOrderSummary } from \"@binaryandthread/store-client\"\n\n/** The minimal client surface the poll loop needs (so tests can mock just this). */\nexport interface PollClientLike {\n poll(sessionId: string): Promise<PollResult>\n}\n\nexport interface PollOptions {\n /** poll cadence in ms (default 5s). */\n intervalMs?: number\n /** hard deadline in ms from the first attempt (default 20 min). After this → \"timeout\". */\n deadlineMs?: number\n /** injectable monotonic clock (default Date.now). */\n now?: () => number\n /** injectable sleep (default a real setTimeout-backed sleep). */\n sleep?: (ms: number) => Promise<void>\n /** optional progress callback (the spinner / status line in the CLI). */\n onTick?: (attempt: number) => void\n}\n\n/** The terminal result of the poll loop. \"timeout\" is OUR status, distinct from the server's. */\nexport interface PollOutcome {\n status: \"placed\" | \"expired\" | \"timeout\"\n order_id: string | null\n /** The settled, discount-aware order summary — present when placed (ENG-39). */\n summary?: SettledOrderSummary | null\n}\n\nconst DEFAULT_INTERVAL_MS = 5_000\nconst DEFAULT_DEADLINE_MS = 20 * 60 * 1_000 // 20 minutes (F-C5)\n\nfunction realSleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms))\n}\n\n/**\n * Poll `client.poll(sessionId)` until the order is placed, the session expires, or the\n * deadline is hit. NEVER loops forever: the deadline is the hard ceiling (F-I3). Cancellable\n * by the caller via SIGINT — the cart stays intact server-side, so a Ctrl-C is fully\n * resumable with `bt checkout --resume` (no local in-flight state to corrupt, F-I1).\n */\nexport async function pollUntilPlaced(\n client: PollClientLike,\n sessionId: string,\n opts: PollOptions = {}\n): Promise<PollOutcome> {\n const intervalMs = opts.intervalMs ?? DEFAULT_INTERVAL_MS\n const deadlineMs = opts.deadlineMs ?? DEFAULT_DEADLINE_MS\n const now = opts.now ?? Date.now\n const sleep = opts.sleep ?? realSleep\n\n const start = now()\n let attempt = 0\n\n for (;;) {\n attempt += 1\n opts.onTick?.(attempt)\n\n const result = await client.poll(sessionId)\n if (result.status === \"placed\") {\n return { status: \"placed\", order_id: result.order_id, summary: result.summary ?? null }\n }\n if (result.status === \"expired\") {\n return { status: \"expired\", order_id: result.order_id }\n }\n\n // Still pending. If we've reached the deadline, stop — never loop forever (F-I3).\n if (now() - start >= deadlineMs) {\n return { status: \"timeout\", order_id: null }\n }\n\n await sleep(intervalMs)\n\n // Re-check the deadline AFTER sleeping so we never start another poll past the cap\n // (bounded overshoot: at most one interval beyond the deadline).\n if (now() - start >= deadlineMs) {\n return { status: \"timeout\", order_id: null }\n }\n }\n}\n"],"mappings":";;;;AAwCA,IAAM,sBAAsB;AAC5B,IAAM,sBAAsB,KAAK,KAAK;AAEtC,SAAS,UAAU,IAA2B;AAC5C,SAAO,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,EAAE,CAAC;AACzD;AAQA,eAAsB,gBACpB,QACA,WACA,OAAoB,CAAC,GACC;AACtB,QAAM,aAAa,KAAK,cAAc;AACtC,QAAM,aAAa,KAAK,cAAc;AACtC,QAAM,MAAM,KAAK,OAAO,KAAK;AAC7B,QAAM,QAAQ,KAAK,SAAS;AAE5B,QAAM,QAAQ,IAAI;AAClB,MAAI,UAAU;AAEd,aAAS;AACP,eAAW;AACX,SAAK,SAAS,OAAO;AAErB,UAAM,SAAS,MAAM,OAAO,KAAK,SAAS;AAC1C,QAAI,OAAO,WAAW,UAAU;AAC9B,aAAO,EAAE,QAAQ,UAAU,UAAU,OAAO,UAAU,SAAS,OAAO,WAAW,KAAK;AAAA,IACxF;AACA,QAAI,OAAO,WAAW,WAAW;AAC/B,aAAO,EAAE,QAAQ,WAAW,UAAU,OAAO,SAAS;AAAA,IACxD;AAGA,QAAI,IAAI,IAAI,SAAS,YAAY;AAC/B,aAAO,EAAE,QAAQ,WAAW,UAAU,KAAK;AAAA,IAC7C;AAEA,UAAM,MAAM,UAAU;AAItB,QAAI,IAAI,IAAI,SAAS,YAAY;AAC/B,aAAO,EAAE,QAAQ,WAAW,UAAU,KAAK;AAAA,IAC7C;AAAA,EACF;AACF;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@binaryandthread/cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "bt — the Binary & Thread terminal store. Buy heavyweight pieces with the ceremony of a production deploy. A dumb, well-tested client over the Store API; cards never touch the terminal.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/config.ts"],"sourcesContent":["// Local config — ~/.config/bt/config.json.\n//\n// Holds ONLY non-sensitive fields (SECURITY G15): cart_id, email, region, defaults.\n// The auth TOKEN is NEVER here — it goes in the OS keychain in Wave 5. Persisting a\n// token in plaintext config would be the exact leak G15 forbids.\n//\n// Writes are ATOMIC (F-B1): write to config.json.tmp then fs.renameSync, so a crash\n// mid-write never corrupts the file (rename is atomic on POSIX + Windows same-volume).\n\nimport * as fs from \"node:fs\"\nimport * as os from \"node:os\"\nimport * as path from \"node:path\"\n\nexport interface BtConfig {\n cart_id?: string\n email?: string\n region?: string\n defaultSize?: string\n defaultColour?: string\n defaultBranch?: string\n /** RUNTIME-ONLY (ENG-21): where the effective region came from this run — flag, saved config, or\n * a locale guess. Deliberately NOT in ALLOWED_KEYS, so saveConfig always strips it. */\n region_source?: \"flag\" | \"config\" | \"locale\"\n}\n\n// Only these keys are ever persisted — anything else (notably `token`) is dropped (G15).\nconst ALLOWED_KEYS: Array<keyof BtConfig> = [\n \"cart_id\",\n \"email\",\n \"region\",\n \"defaultSize\",\n \"defaultColour\",\n \"defaultBranch\",\n]\n\n/** The config directory: $XDG_CONFIG_HOME/bt or ~/.config/bt. */\nexport function configDir(): string {\n const xdg = process.env.XDG_CONFIG_HOME\n const base = xdg && xdg.trim() ? xdg : path.join(os.homedir(), \".config\")\n return path.join(base, \"bt\")\n}\n\nexport function configPath(): string {\n return path.join(configDir(), \"config.json\")\n}\n\n/** Load the config, or {} if none / unreadable. Never throws. */\nexport function loadConfig(): BtConfig {\n try {\n const raw = fs.readFileSync(configPath(), \"utf8\")\n const parsed = JSON.parse(raw) as Record<string, unknown>\n return sanitize(parsed)\n } catch {\n return {}\n }\n}\n\n/** Merge-and-save the given fields. Strips any non-allowlisted key (G15). Atomic (F-B1). */\nexport function saveConfig(patch: Partial<BtConfig>): BtConfig {\n const merged = { ...loadConfig(), ...sanitize(patch as Record<string, unknown>) }\n const dir = configDir()\n fs.mkdirSync(dir, { recursive: true })\n const target = configPath()\n const tmp = target + \".tmp\"\n // write to a temp file then atomically rename (F-B1)\n fs.writeFileSync(tmp, JSON.stringify(merged, null, 2), { encoding: \"utf8\", mode: 0o600 })\n fs.renameSync(tmp, target)\n return merged\n}\n\n/** Wipe the local config file (G15 erasure — `bt forget`). Never throws. */\nexport function forgetConfig(): void {\n try {\n fs.rmSync(configPath(), { force: true })\n } catch {\n /* ignore */\n }\n}\n\n/** Keep only allowlisted, non-sensitive keys (drops `token` and anything unexpected). */\nfunction sanitize(obj: Record<string, unknown>): BtConfig {\n const out: BtConfig = {}\n for (const key of ALLOWED_KEYS) {\n const v = obj[key]\n if (typeof v === \"string\" && v.length > 0) {\n out[key] = v\n }\n }\n return out\n}\n"],"mappings":";;;AASA,YAAY,QAAQ;AACpB,YAAY,QAAQ;AACpB,YAAY,UAAU;AAetB,IAAM,eAAsC;AAAA,EAC1C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAGO,SAAS,YAAoB;AAClC,QAAM,MAAM,QAAQ,IAAI;AACxB,QAAM,OAAO,OAAO,IAAI,KAAK,IAAI,MAAW,UAAQ,WAAQ,GAAG,SAAS;AACxE,SAAY,UAAK,MAAM,IAAI;AAC7B;AAEO,SAAS,aAAqB;AACnC,SAAY,UAAK,UAAU,GAAG,aAAa;AAC7C;AAGO,SAAS,aAAuB;AACrC,MAAI;AACF,UAAM,MAAS,gBAAa,WAAW,GAAG,MAAM;AAChD,UAAM,SAAS,KAAK,MAAM,GAAG;AAC7B,WAAO,SAAS,MAAM;AAAA,EACxB,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;AAGO,SAAS,WAAW,OAAoC;AAC7D,QAAM,SAAS,EAAE,GAAG,WAAW,GAAG,GAAG,SAAS,KAAgC,EAAE;AAChF,QAAM,MAAM,UAAU;AACtB,EAAG,aAAU,KAAK,EAAE,WAAW,KAAK,CAAC;AACrC,QAAM,SAAS,WAAW;AAC1B,QAAM,MAAM,SAAS;AAErB,EAAG,iBAAc,KAAK,KAAK,UAAU,QAAQ,MAAM,CAAC,GAAG,EAAE,UAAU,QAAQ,MAAM,IAAM,CAAC;AACxF,EAAG,cAAW,KAAK,MAAM;AACzB,SAAO;AACT;AAGO,SAAS,eAAqB;AACnC,MAAI;AACF,IAAG,UAAO,WAAW,GAAG,EAAE,OAAO,KAAK,CAAC;AAAA,EACzC,QAAQ;AAAA,EAER;AACF;AAGA,SAAS,SAAS,KAAwC;AACxD,QAAM,MAAgB,CAAC;AACvB,aAAW,OAAO,cAAc;AAC9B,UAAM,IAAI,IAAI,GAAG;AACjB,QAAI,OAAO,MAAM,YAAY,EAAE,SAAS,GAAG;AACzC,UAAI,GAAG,IAAI;AAAA,IACb;AAAA,EACF;AACA,SAAO;AACT;","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/poll.ts"],"sourcesContent":["// Bounded poll loop — the checkout-completion watcher (F-C5 / F-I3).\n//\n// After the browser hop, the CLI polls the bt_session until the order is placed. This loop\n// is the single most safety-critical control flow in the write path that runs unattended:\n// it MUST be bounded so a closed-browser / never-returns buyer can never hang the terminal\n// forever (F-I3). It polls every `intervalMs` (5s), stops the instant the session is\n// \"placed\" or \"expired\", and stops at a `deadlineMs` (20-min cap) returning \"timeout\".\n//\n// The clock + sleep are INJECTED (defaults wired to the real ones) so the deadline is\n// deterministically testable without burning 20 minutes of wall-clock — and so nothing\n// authoritative is ever computed from an un-injectable clock (F-I5).\n\nimport type { PollResult } from \"@binaryandthread/store-client\"\n\n/** The minimal client surface the poll loop needs (so tests can mock just this). */\nexport interface PollClientLike {\n poll(sessionId: string): Promise<PollResult>\n}\n\nexport interface PollOptions {\n /** poll cadence in ms (default 5s). */\n intervalMs?: number\n /** hard deadline in ms from the first attempt (default 20 min). After this → \"timeout\". */\n deadlineMs?: number\n /** injectable monotonic clock (default Date.now). */\n now?: () => number\n /** injectable sleep (default a real setTimeout-backed sleep). */\n sleep?: (ms: number) => Promise<void>\n /** optional progress callback (the spinner / status line in the CLI). */\n onTick?: (attempt: number) => void\n}\n\n/** The terminal result of the poll loop. \"timeout\" is OUR status, distinct from the server's. */\nexport interface PollOutcome {\n status: \"placed\" | \"expired\" | \"timeout\"\n order_id: string | null\n}\n\nconst DEFAULT_INTERVAL_MS = 5_000\nconst DEFAULT_DEADLINE_MS = 20 * 60 * 1_000 // 20 minutes (F-C5)\n\nfunction realSleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms))\n}\n\n/**\n * Poll `client.poll(sessionId)` until the order is placed, the session expires, or the\n * deadline is hit. NEVER loops forever: the deadline is the hard ceiling (F-I3). Cancellable\n * by the caller via SIGINT — the cart stays intact server-side, so a Ctrl-C is fully\n * resumable with `bt checkout --resume` (no local in-flight state to corrupt, F-I1).\n */\nexport async function pollUntilPlaced(\n client: PollClientLike,\n sessionId: string,\n opts: PollOptions = {}\n): Promise<PollOutcome> {\n const intervalMs = opts.intervalMs ?? DEFAULT_INTERVAL_MS\n const deadlineMs = opts.deadlineMs ?? DEFAULT_DEADLINE_MS\n const now = opts.now ?? Date.now\n const sleep = opts.sleep ?? realSleep\n\n const start = now()\n let attempt = 0\n\n for (;;) {\n attempt += 1\n opts.onTick?.(attempt)\n\n const result = await client.poll(sessionId)\n if (result.status === \"placed\") {\n return { status: \"placed\", order_id: result.order_id }\n }\n if (result.status === \"expired\") {\n return { status: \"expired\", order_id: result.order_id }\n }\n\n // Still pending. If we've reached the deadline, stop — never loop forever (F-I3).\n if (now() - start >= deadlineMs) {\n return { status: \"timeout\", order_id: null }\n }\n\n await sleep(intervalMs)\n\n // Re-check the deadline AFTER sleeping so we never start another poll past the cap\n // (bounded overshoot: at most one interval beyond the deadline).\n if (now() - start >= deadlineMs) {\n return { status: \"timeout\", order_id: null }\n }\n }\n}\n"],"mappings":";;;;AAsCA,IAAM,sBAAsB;AAC5B,IAAM,sBAAsB,KAAK,KAAK;AAEtC,SAAS,UAAU,IAA2B;AAC5C,SAAO,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,EAAE,CAAC;AACzD;AAQA,eAAsB,gBACpB,QACA,WACA,OAAoB,CAAC,GACC;AACtB,QAAM,aAAa,KAAK,cAAc;AACtC,QAAM,aAAa,KAAK,cAAc;AACtC,QAAM,MAAM,KAAK,OAAO,KAAK;AAC7B,QAAM,QAAQ,KAAK,SAAS;AAE5B,QAAM,QAAQ,IAAI;AAClB,MAAI,UAAU;AAEd,aAAS;AACP,eAAW;AACX,SAAK,SAAS,OAAO;AAErB,UAAM,SAAS,MAAM,OAAO,KAAK,SAAS;AAC1C,QAAI,OAAO,WAAW,UAAU;AAC9B,aAAO,EAAE,QAAQ,UAAU,UAAU,OAAO,SAAS;AAAA,IACvD;AACA,QAAI,OAAO,WAAW,WAAW;AAC/B,aAAO,EAAE,QAAQ,WAAW,UAAU,OAAO,SAAS;AAAA,IACxD;AAGA,QAAI,IAAI,IAAI,SAAS,YAAY;AAC/B,aAAO,EAAE,QAAQ,WAAW,UAAU,KAAK;AAAA,IAC7C;AAEA,UAAM,MAAM,UAAU;AAItB,QAAI,IAAI,IAAI,SAAS,YAAY;AAC/B,aAAO,EAAE,QAAQ,WAAW,UAAU,KAAK;AAAA,IAC7C;AAAA,EACF;AACF;","names":[]}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|