beads-ui 0.4.4 → 0.6.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/CHANGES.md +51 -0
- package/README.md +4 -2
- package/app/index.html +11 -0
- package/app/main.bundle.js +242 -225
- package/app/main.bundle.js.map +4 -4
- package/app/styles.css +135 -0
- package/package.json +18 -18
- package/server/cli/commands.js +14 -4
- package/server/cli/daemon.js +11 -2
- package/server/cli/index.js +29 -11
- package/server/cli/usage.js +5 -3
- package/server/config.js +2 -1
- package/server/index.js +10 -0
- package/server/ws.js +61 -21
package/app/styles.css
CHANGED
|
@@ -196,6 +196,28 @@ a {
|
|
|
196
196
|
align-items: center;
|
|
197
197
|
gap: var(--space-5);
|
|
198
198
|
}
|
|
199
|
+
.header-loading {
|
|
200
|
+
display: inline-flex;
|
|
201
|
+
align-items: center;
|
|
202
|
+
justify-content: center;
|
|
203
|
+
width: 22px;
|
|
204
|
+
height: 22px;
|
|
205
|
+
color: var(--muted);
|
|
206
|
+
}
|
|
207
|
+
.header-loading__spinner {
|
|
208
|
+
width: 16px;
|
|
209
|
+
height: 16px;
|
|
210
|
+
border-radius: 999px;
|
|
211
|
+
border: 2px solid color-mix(in srgb, var(--muted) 55%, transparent);
|
|
212
|
+
border-top-color: var(--fg);
|
|
213
|
+
animation: header-spin 880ms linear infinite;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
@keyframes header-spin {
|
|
217
|
+
to {
|
|
218
|
+
transform: rotate(360deg);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
199
221
|
.theme-toggle {
|
|
200
222
|
display: inline-flex;
|
|
201
223
|
align-items: center;
|
|
@@ -967,6 +989,23 @@ input.inline-edit:focus {
|
|
|
967
989
|
background: inherit;
|
|
968
990
|
backdrop-filter: saturate(140%) blur(4px);
|
|
969
991
|
}
|
|
992
|
+
.board-column__title {
|
|
993
|
+
display: inline-flex;
|
|
994
|
+
align-items: center;
|
|
995
|
+
gap: var(--space-3);
|
|
996
|
+
}
|
|
997
|
+
.board-column__title-text {
|
|
998
|
+
font-weight: inherit;
|
|
999
|
+
}
|
|
1000
|
+
.board-column__count {
|
|
1001
|
+
background: color-mix(in srgb, var(--panel-bg) 88%, var(--muted));
|
|
1002
|
+
color: color-mix(in srgb, var(--muted) 82%, #000);
|
|
1003
|
+
border-color: color-mix(in srgb, var(--muted) 28%, transparent);
|
|
1004
|
+
min-width: 28px;
|
|
1005
|
+
text-align: center;
|
|
1006
|
+
letter-spacing: 0.02em;
|
|
1007
|
+
font-variant-numeric: tabular-nums;
|
|
1008
|
+
}
|
|
970
1009
|
/* Small, unobtrusive select for closed filter */
|
|
971
1010
|
.board-closed-filter {
|
|
972
1011
|
margin: calc(var(--space-4) * -1) 0;
|
|
@@ -1440,6 +1479,102 @@ html[data-theme='dark'] {
|
|
|
1440
1479
|
line-height: 1.2;
|
|
1441
1480
|
}
|
|
1442
1481
|
|
|
1482
|
+
/* --- Fatal Error Dialog --- */
|
|
1483
|
+
#fatal-error-dialog {
|
|
1484
|
+
padding: 0;
|
|
1485
|
+
border: 1px solid var(--border);
|
|
1486
|
+
border-radius: 12px;
|
|
1487
|
+
background:
|
|
1488
|
+
linear-gradient(
|
|
1489
|
+
180deg,
|
|
1490
|
+
color-mix(in srgb, var(--panel-bg) 82%, transparent),
|
|
1491
|
+
var(--panel-bg)
|
|
1492
|
+
),
|
|
1493
|
+
radial-gradient(
|
|
1494
|
+
circle at 20% 0%,
|
|
1495
|
+
color-mix(in srgb, #fca5a5 48%, transparent) 0%,
|
|
1496
|
+
transparent 42%
|
|
1497
|
+
);
|
|
1498
|
+
color: var(--fg);
|
|
1499
|
+
width: min(640px, 94vw);
|
|
1500
|
+
max-height: 90vh;
|
|
1501
|
+
margin: 4vh auto;
|
|
1502
|
+
overflow: hidden;
|
|
1503
|
+
box-shadow: 0 18px 48px -16px rgba(17, 24, 39, 0.35);
|
|
1504
|
+
}
|
|
1505
|
+
#fatal-error-dialog::backdrop {
|
|
1506
|
+
background: color-mix(in srgb, #0f172a 55%, transparent);
|
|
1507
|
+
backdrop-filter: blur(1px);
|
|
1508
|
+
}
|
|
1509
|
+
.fatal-error {
|
|
1510
|
+
display: flex;
|
|
1511
|
+
align-items: flex-start;
|
|
1512
|
+
gap: 16px;
|
|
1513
|
+
padding: 18px 20px 20px;
|
|
1514
|
+
}
|
|
1515
|
+
.fatal-error__icon {
|
|
1516
|
+
width: 40px;
|
|
1517
|
+
height: 40px;
|
|
1518
|
+
border-radius: 12px;
|
|
1519
|
+
display: inline-flex;
|
|
1520
|
+
align-items: center;
|
|
1521
|
+
justify-content: center;
|
|
1522
|
+
font-weight: 800;
|
|
1523
|
+
font-size: 20px;
|
|
1524
|
+
color: #fff;
|
|
1525
|
+
background: linear-gradient(
|
|
1526
|
+
135deg,
|
|
1527
|
+
color-mix(in srgb, var(--type-bug-base) 78%, #000),
|
|
1528
|
+
color-mix(in srgb, var(--type-bug-base) 58%, #111)
|
|
1529
|
+
);
|
|
1530
|
+
box-shadow: 0 10px 24px -12px rgba(159, 32, 17, 0.6);
|
|
1531
|
+
}
|
|
1532
|
+
.fatal-error__body {
|
|
1533
|
+
flex: 1;
|
|
1534
|
+
display: flex;
|
|
1535
|
+
flex-direction: column;
|
|
1536
|
+
gap: 6px;
|
|
1537
|
+
}
|
|
1538
|
+
.fatal-error__eyebrow {
|
|
1539
|
+
margin: 0;
|
|
1540
|
+
text-transform: uppercase;
|
|
1541
|
+
letter-spacing: 0.08em;
|
|
1542
|
+
font-size: 11px;
|
|
1543
|
+
color: color-mix(in srgb, var(--type-bug-base) 64%, #111);
|
|
1544
|
+
font-weight: 700;
|
|
1545
|
+
}
|
|
1546
|
+
.fatal-error__title {
|
|
1547
|
+
margin: 0;
|
|
1548
|
+
font-size: 20px;
|
|
1549
|
+
letter-spacing: 0.01em;
|
|
1550
|
+
}
|
|
1551
|
+
.fatal-error__message {
|
|
1552
|
+
margin: 0;
|
|
1553
|
+
color: color-mix(in srgb, var(--fg) 92%, #111);
|
|
1554
|
+
font-size: 14px;
|
|
1555
|
+
line-height: 1.5;
|
|
1556
|
+
}
|
|
1557
|
+
.fatal-error__detail {
|
|
1558
|
+
margin: 6px 0 0;
|
|
1559
|
+
background: var(--pre-bg);
|
|
1560
|
+
color: var(--pre-fg);
|
|
1561
|
+
border-radius: 8px;
|
|
1562
|
+
padding: 10px 12px;
|
|
1563
|
+
max-height: 220px;
|
|
1564
|
+
overflow: auto;
|
|
1565
|
+
font-size: 12px;
|
|
1566
|
+
border: 1px solid color-mix(in srgb, var(--pre-bg) 45%, #1f2937);
|
|
1567
|
+
white-space: pre-wrap;
|
|
1568
|
+
word-break: break-word;
|
|
1569
|
+
}
|
|
1570
|
+
.fatal-error__actions {
|
|
1571
|
+
display: flex;
|
|
1572
|
+
align-items: center;
|
|
1573
|
+
gap: 10px;
|
|
1574
|
+
margin-top: 10px;
|
|
1575
|
+
flex-wrap: wrap;
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1443
1578
|
/* Sidebar control aesthetics */
|
|
1444
1579
|
#detail-root .props-card input[type='text'] {
|
|
1445
1580
|
border: 1px solid var(--control-border);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "beads-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Local UI for Beads — Collaborate on issues with your coding agent.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent",
|
|
@@ -18,15 +18,15 @@
|
|
|
18
18
|
"node": ">=22"
|
|
19
19
|
},
|
|
20
20
|
"scripts": {
|
|
21
|
-
"all": "npm run lint && npm run
|
|
21
|
+
"all": "npm run lint && npm run tsc && npm test && npm run prettier:check",
|
|
22
22
|
"start": "node server/index.js --debug",
|
|
23
23
|
"build": "node scripts/build-frontend.js",
|
|
24
24
|
"test": "vitest run",
|
|
25
25
|
"test:watch": "vitest",
|
|
26
|
-
"
|
|
26
|
+
"tsc": "tsc -p tsconfig.json --noEmit",
|
|
27
27
|
"lint": "eslint --ext .js .",
|
|
28
|
-
"
|
|
29
|
-
"
|
|
28
|
+
"prettier:write": "prettier --write .",
|
|
29
|
+
"prettier:check": "prettier --check .",
|
|
30
30
|
"preversion": "npm run all",
|
|
31
31
|
"version": "changes --commits --footer",
|
|
32
32
|
"postversion": "git push --follow-tags && npm publish",
|
|
@@ -36,29 +36,29 @@
|
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"debug": "^4.4.3",
|
|
38
38
|
"dompurify": "^3.3.0",
|
|
39
|
-
"express": "^5.1
|
|
39
|
+
"express": "^5.2.1",
|
|
40
40
|
"lit-html": "^3.3.1",
|
|
41
|
-
"marked": "^
|
|
41
|
+
"marked": "^17.0.1",
|
|
42
42
|
"ws": "^8.18.3"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
|
-
"@eslint/js": "^9.
|
|
45
|
+
"@eslint/js": "^9.39.1",
|
|
46
46
|
"@studio/changes": "^3.0.0",
|
|
47
|
-
"@trivago/prettier-plugin-sort-imports": "^
|
|
47
|
+
"@trivago/prettier-plugin-sort-imports": "^6.0.0",
|
|
48
48
|
"@types/debug": "^4.1.12",
|
|
49
|
-
"@types/express": "^5.0.
|
|
50
|
-
"@types/node": "^22.
|
|
49
|
+
"@types/express": "^5.0.6",
|
|
50
|
+
"@types/node": "^22.19.1",
|
|
51
51
|
"@types/ws": "^8.18.1",
|
|
52
|
-
"esbuild": "^0.
|
|
53
|
-
"eslint": "^9.
|
|
52
|
+
"esbuild": "^0.27.1",
|
|
53
|
+
"eslint": "^9.39.1",
|
|
54
54
|
"eslint-plugin-import": "^2.29.1",
|
|
55
|
-
"eslint-plugin-jsdoc": "^61.1
|
|
55
|
+
"eslint-plugin-jsdoc": "^61.4.1",
|
|
56
56
|
"eslint-plugin-n": "^17.9.0",
|
|
57
|
-
"globals": "^16.
|
|
58
|
-
"jsdom": "^27.0
|
|
59
|
-
"prettier": "^3.
|
|
57
|
+
"globals": "^16.5.0",
|
|
58
|
+
"jsdom": "^27.2.0",
|
|
59
|
+
"prettier": "^3.7.4",
|
|
60
60
|
"typescript": "^5.6.3",
|
|
61
|
-
"vitest": "^4.0.
|
|
61
|
+
"vitest": "^4.0.15"
|
|
62
62
|
},
|
|
63
63
|
"files": [
|
|
64
64
|
"app/index.html",
|
package/server/cli/commands.js
CHANGED
|
@@ -14,11 +14,9 @@ import { openUrl, waitForServer } from './open.js';
|
|
|
14
14
|
* - Spawns a detached server process, writes PID file, returns 0.
|
|
15
15
|
* - If already running (PID file present and process alive), prints URL and returns 0.
|
|
16
16
|
*
|
|
17
|
+
* @param {{ open?: boolean, is_debug?: boolean, host?: string, port?: number }} [options]
|
|
17
18
|
* @returns {Promise<number>} Exit code (0 on success)
|
|
18
19
|
*/
|
|
19
|
-
/**
|
|
20
|
-
* @param {{ open?: boolean, is_debug?: boolean }} [options]
|
|
21
|
-
*/
|
|
22
20
|
export async function handleStart(options) {
|
|
23
21
|
// Default: do not open a browser unless explicitly requested via `open: true`.
|
|
24
22
|
const should_open = options?.open === true;
|
|
@@ -32,7 +30,19 @@ export async function handleStart(options) {
|
|
|
32
30
|
removePidFile();
|
|
33
31
|
}
|
|
34
32
|
|
|
35
|
-
|
|
33
|
+
// Set env vars in current process so getConfig() reflects the overrides
|
|
34
|
+
if (options?.host) {
|
|
35
|
+
process.env.HOST = options.host;
|
|
36
|
+
}
|
|
37
|
+
if (options?.port) {
|
|
38
|
+
process.env.PORT = String(options.port);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const started = startDaemon({
|
|
42
|
+
is_debug: options?.is_debug,
|
|
43
|
+
host: options?.host,
|
|
44
|
+
port: options?.port
|
|
45
|
+
});
|
|
36
46
|
if (started && started.pid > 0) {
|
|
37
47
|
printServerUrl();
|
|
38
48
|
// Auto-open the browser once for a fresh daemon start
|
package/server/cli/daemon.js
CHANGED
|
@@ -140,7 +140,7 @@ export function getServerEntryPath() {
|
|
|
140
140
|
* Spawn the server as a detached daemon, redirecting stdio to the log file.
|
|
141
141
|
* Writes the PID file upon success.
|
|
142
142
|
*
|
|
143
|
-
* @param {{ is_debug?: boolean }} [options]
|
|
143
|
+
* @param {{ is_debug?: boolean, host?: string, port?: number }} [options]
|
|
144
144
|
* @returns {{ pid: number } | null} Returns child PID on success; null on failure.
|
|
145
145
|
*/
|
|
146
146
|
export function startDaemon(options = {}) {
|
|
@@ -160,10 +160,19 @@ export function startDaemon(options = {}) {
|
|
|
160
160
|
log_fd = -1;
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
+
/** @type {Record<string, string | undefined>} */
|
|
164
|
+
const spawn_env = { ...process.env };
|
|
165
|
+
if (options.host) {
|
|
166
|
+
spawn_env.HOST = options.host;
|
|
167
|
+
}
|
|
168
|
+
if (options.port) {
|
|
169
|
+
spawn_env.PORT = String(options.port);
|
|
170
|
+
}
|
|
171
|
+
|
|
163
172
|
/** @type {SpawnOptions} */
|
|
164
173
|
const opts = {
|
|
165
174
|
detached: true,
|
|
166
|
-
env:
|
|
175
|
+
env: spawn_env,
|
|
167
176
|
stdio: log_fd >= 0 ? ['ignore', log_fd, log_fd] : 'ignore',
|
|
168
177
|
windowsHide: true
|
|
169
178
|
};
|
package/server/cli/index.js
CHANGED
|
@@ -3,18 +3,21 @@ import { handleRestart, handleStart, handleStop } from './commands.js';
|
|
|
3
3
|
import { printUsage } from './usage.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Parse argv into a command token and
|
|
6
|
+
* Parse argv into a command token, flags, and options.
|
|
7
7
|
*
|
|
8
8
|
* @param {string[]} args
|
|
9
|
-
* @returns {{ command: string | null, flags: string[] }}
|
|
9
|
+
* @returns {{ command: string | null, flags: string[], options: { host?: string, port?: number } }}
|
|
10
10
|
*/
|
|
11
11
|
export function parseArgs(args) {
|
|
12
12
|
/** @type {string[]} */
|
|
13
13
|
const flags = [];
|
|
14
14
|
/** @type {string | null} */
|
|
15
15
|
let command = null;
|
|
16
|
+
/** @type {{ host?: string, port?: number }} */
|
|
17
|
+
const options = {};
|
|
16
18
|
|
|
17
|
-
for (
|
|
19
|
+
for (let i = 0; i < args.length; i++) {
|
|
20
|
+
const token = args[i];
|
|
18
21
|
if (token === '--help' || token === '-h') {
|
|
19
22
|
flags.push('help');
|
|
20
23
|
continue;
|
|
@@ -27,6 +30,17 @@ export function parseArgs(args) {
|
|
|
27
30
|
flags.push('open');
|
|
28
31
|
continue;
|
|
29
32
|
}
|
|
33
|
+
if (token === '--host' && i + 1 < args.length) {
|
|
34
|
+
options.host = args[++i];
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (token === '--port' && i + 1 < args.length) {
|
|
38
|
+
const port_value = Number.parseInt(args[++i], 10);
|
|
39
|
+
if (Number.isFinite(port_value) && port_value > 0) {
|
|
40
|
+
options.port = port_value;
|
|
41
|
+
}
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
30
44
|
if (
|
|
31
45
|
!command &&
|
|
32
46
|
(token === 'start' || token === 'stop' || token === 'restart')
|
|
@@ -37,7 +51,7 @@ export function parseArgs(args) {
|
|
|
37
51
|
// Ignore unrecognized tokens for now; future flags may be parsed here.
|
|
38
52
|
}
|
|
39
53
|
|
|
40
|
-
return { command, flags };
|
|
54
|
+
return { command, flags, options };
|
|
41
55
|
}
|
|
42
56
|
|
|
43
57
|
/**
|
|
@@ -48,7 +62,7 @@ export function parseArgs(args) {
|
|
|
48
62
|
* @returns {Promise<number>}
|
|
49
63
|
*/
|
|
50
64
|
export async function main(args) {
|
|
51
|
-
const { command, flags } = parseArgs(args);
|
|
65
|
+
const { command, flags, options } = parseArgs(args);
|
|
52
66
|
|
|
53
67
|
const is_debug = flags.includes('debug');
|
|
54
68
|
if (is_debug) {
|
|
@@ -68,21 +82,25 @@ export async function main(args) {
|
|
|
68
82
|
/**
|
|
69
83
|
* Default behavior: do NOT open a browser. `--open` explicitly opens.
|
|
70
84
|
*/
|
|
71
|
-
const
|
|
85
|
+
const start_options = {
|
|
72
86
|
open: flags.includes('open'),
|
|
73
|
-
is_debug: is_debug || Boolean(process.env.DEBUG)
|
|
87
|
+
is_debug: is_debug || Boolean(process.env.DEBUG),
|
|
88
|
+
host: options.host,
|
|
89
|
+
port: options.port
|
|
74
90
|
};
|
|
75
|
-
return await handleStart(
|
|
91
|
+
return await handleStart(start_options);
|
|
76
92
|
}
|
|
77
93
|
if (command === 'stop') {
|
|
78
94
|
return await handleStop();
|
|
79
95
|
}
|
|
80
96
|
if (command === 'restart') {
|
|
81
|
-
const
|
|
97
|
+
const restart_options = {
|
|
82
98
|
open: flags.includes('open'),
|
|
83
|
-
is_debug: is_debug || Boolean(process.env.DEBUG)
|
|
99
|
+
is_debug: is_debug || Boolean(process.env.DEBUG),
|
|
100
|
+
host: options.host,
|
|
101
|
+
port: options.port
|
|
84
102
|
};
|
|
85
|
-
return await handleRestart(
|
|
103
|
+
return await handleRestart(restart_options);
|
|
86
104
|
}
|
|
87
105
|
|
|
88
106
|
// Unknown command path (should not happen due to parseArgs guard)
|
package/server/cli/usage.js
CHANGED
|
@@ -13,9 +13,11 @@ export function printUsage(out_stream) {
|
|
|
13
13
|
' restart Restart the UI server',
|
|
14
14
|
'',
|
|
15
15
|
'Options:',
|
|
16
|
-
' -h, --help
|
|
17
|
-
' -d, --debug
|
|
18
|
-
' --open
|
|
16
|
+
' -h, --help Show this help message',
|
|
17
|
+
' -d, --debug Enable debug logging',
|
|
18
|
+
' --open Open the browser after start/restart',
|
|
19
|
+
' --host <addr> Bind to a specific host (default: 127.0.0.1)',
|
|
20
|
+
' --port <num> Bind to a specific port (default: 3000)',
|
|
19
21
|
''
|
|
20
22
|
];
|
|
21
23
|
for (const line of lines) {
|
package/server/config.js
CHANGED
package/server/index.js
CHANGED
|
@@ -10,6 +10,16 @@ if (process.argv.includes('--debug') || process.argv.includes('-d')) {
|
|
|
10
10
|
enableAllDebug();
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
// Parse --host and --port from argv and set env vars before getConfig()
|
|
14
|
+
for (let i = 0; i < process.argv.length; i++) {
|
|
15
|
+
if (process.argv[i] === '--host' && process.argv[i + 1]) {
|
|
16
|
+
process.env.HOST = process.argv[++i];
|
|
17
|
+
}
|
|
18
|
+
if (process.argv[i] === '--port' && process.argv[i + 1]) {
|
|
19
|
+
process.env.PORT = process.argv[++i];
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
13
23
|
const config = getConfig();
|
|
14
24
|
const app = createApp(config);
|
|
15
25
|
const server = createServer(app);
|
package/server/ws.js
CHANGED
|
@@ -534,7 +534,8 @@ export async function handleMessage(ws, data) {
|
|
|
534
534
|
|
|
535
535
|
// subscribe-list: payload { id: string, type: string, params?: object }
|
|
536
536
|
if (req.type === 'subscribe-list') {
|
|
537
|
-
|
|
537
|
+
const payload_id = /** @type {any} */ (req.payload)?.id || '';
|
|
538
|
+
log('subscribe-list %s', payload_id);
|
|
538
539
|
const validation = validateSubscribeListPayload(
|
|
539
540
|
/** @type {any} */ (req.payload || {})
|
|
540
541
|
);
|
|
@@ -546,31 +547,70 @@ export async function handleMessage(ws, data) {
|
|
|
546
547
|
}
|
|
547
548
|
const client_id = validation.id;
|
|
548
549
|
const spec = validation.spec;
|
|
550
|
+
const key = keyOf(spec);
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Reply with an error and avoid attaching the subscription when
|
|
554
|
+
* initialization fails.
|
|
555
|
+
*
|
|
556
|
+
* @param {string} code
|
|
557
|
+
* @param {string} message
|
|
558
|
+
* @param {Record<string, unknown>|undefined} details
|
|
559
|
+
*/
|
|
560
|
+
const replyWithError = (code, message, details = undefined) => {
|
|
561
|
+
ws.send(JSON.stringify(makeError(req, code, message, details)));
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
/** @type {Awaited<ReturnType<typeof fetchListForSubscription>> | null} */
|
|
565
|
+
let initial = null;
|
|
566
|
+
try {
|
|
567
|
+
initial = await fetchListForSubscription(spec);
|
|
568
|
+
} catch (err) {
|
|
569
|
+
log('subscribe-list snapshot error for %s: %o', key, err);
|
|
570
|
+
const message =
|
|
571
|
+
(err && /** @type {any} */ (err).message) || 'Failed to load list';
|
|
572
|
+
replyWithError('bd_error', String(message), { key });
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
if (!initial.ok) {
|
|
577
|
+
log(
|
|
578
|
+
'initial snapshot failed for %s: %s %o',
|
|
579
|
+
key,
|
|
580
|
+
initial.error.message,
|
|
581
|
+
initial.error
|
|
582
|
+
);
|
|
583
|
+
const details = { ...(initial.error.details || {}), key };
|
|
584
|
+
replyWithError(initial.error.code, initial.error.message, details);
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
|
|
549
588
|
const s = ensureSubs(ws);
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
// Send an initial snapshot for this client id only and store items
|
|
589
|
+
const { key: attached_key } = registry.attach(spec, ws);
|
|
590
|
+
s.list_subs?.set(client_id, { key: attached_key, spec });
|
|
591
|
+
|
|
554
592
|
try {
|
|
555
|
-
await registry.withKeyLock(
|
|
556
|
-
const
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
res.error
|
|
563
|
-
);
|
|
564
|
-
return;
|
|
565
|
-
}
|
|
566
|
-
const items = applyClosedIssuesFilter(spec, res.items);
|
|
567
|
-
void registry.applyItems(key, items);
|
|
568
|
-
emitSubscriptionSnapshot(ws, client_id, key, items);
|
|
593
|
+
await registry.withKeyLock(attached_key, async () => {
|
|
594
|
+
const items = applyClosedIssuesFilter(
|
|
595
|
+
spec,
|
|
596
|
+
initial ? initial.items : []
|
|
597
|
+
);
|
|
598
|
+
void registry.applyItems(attached_key, items);
|
|
599
|
+
emitSubscriptionSnapshot(ws, client_id, attached_key, items);
|
|
569
600
|
});
|
|
570
601
|
} catch (err) {
|
|
571
|
-
log('subscribe-list snapshot error for %s: %o',
|
|
602
|
+
log('subscribe-list snapshot error for %s: %o', attached_key, err);
|
|
603
|
+
s.list_subs?.delete(client_id);
|
|
604
|
+
try {
|
|
605
|
+
registry.detach(spec, ws);
|
|
606
|
+
} catch {
|
|
607
|
+
// ignore detach errors
|
|
608
|
+
}
|
|
609
|
+
replyWithError('bd_error', 'Failed to publish snapshot', { key });
|
|
610
|
+
return;
|
|
572
611
|
}
|
|
573
|
-
|
|
612
|
+
|
|
613
|
+
ws.send(JSON.stringify(makeOk(req, { id: client_id, key: attached_key })));
|
|
574
614
|
return;
|
|
575
615
|
}
|
|
576
616
|
|