beads-ui 0.3.0 → 0.3.1

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.
Files changed (45) hide show
  1. package/CHANGES.md +9 -0
  2. package/app/data/list-selectors.js +5 -0
  3. package/app/data/providers.js +2 -0
  4. package/app/data/sort.js +2 -0
  5. package/app/data/subscription-issue-store.js +2 -2
  6. package/app/data/subscription-issue-stores.js +31 -5
  7. package/app/data/subscriptions-store.js +9 -1
  8. package/app/main.js +4 -0
  9. package/app/protocol.js +13 -3
  10. package/app/router.js +3 -0
  11. package/app/state.js +2 -0
  12. package/app/utils/issue-id-renderer.js +2 -1
  13. package/app/utils/issue-id.js +1 -0
  14. package/app/utils/issue-type.js +2 -0
  15. package/app/utils/issue-url.js +1 -0
  16. package/app/utils/markdown.js +4 -10
  17. package/app/utils/priority-badge.js +1 -0
  18. package/app/utils/status-badge.js +1 -0
  19. package/app/utils/status.js +2 -0
  20. package/app/utils/toast.js +1 -0
  21. package/app/utils/type-badge.js +1 -0
  22. package/app/views/board.js +8 -6
  23. package/app/views/detail.js +3 -0
  24. package/app/views/epics.js +6 -5
  25. package/app/views/issue-dialog.js +1 -0
  26. package/app/views/issue-row.js +1 -0
  27. package/app/views/list.js +4 -0
  28. package/app/views/nav.js +1 -0
  29. package/app/views/new-issue-dialog.js +3 -0
  30. package/app/ws.js +4 -1
  31. package/package.json +2 -3
  32. package/server/app.js +2 -0
  33. package/server/bd.js +4 -0
  34. package/server/cli/commands.js +4 -0
  35. package/server/cli/daemon.js +7 -0
  36. package/server/cli/index.js +2 -0
  37. package/server/cli/open.js +3 -0
  38. package/server/cli/usage.js +1 -0
  39. package/server/config.js +3 -2
  40. package/server/db.js +3 -0
  41. package/server/list-adapters.js +9 -3
  42. package/server/subscriptions.js +12 -0
  43. package/server/validators.js +2 -0
  44. package/server/watcher.js +3 -0
  45. package/server/ws.js +9 -0
package/CHANGES.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changes
2
2
 
3
+ ## 0.3.1
4
+
5
+ - [`3912ae5`](https://github.com/mantoni/beads-ui/commit/3912ae552b1cc97e61fbaaa0815ca77675c542e4)
6
+ Status filter intermittently not applied on Issues screen
7
+ - [`a160484`](https://github.com/mantoni/beads-ui/commit/a16048479d1d7d61ed4ad4e53365a5736eb053af)
8
+ Upgrade eslint-plugin-jsdoc and switch config
9
+
10
+ _Released by [Maximilian Antoni](https://github.com/mantoni) on 2025-10-27._
11
+
3
12
  ## 0.3.0
4
13
 
5
14
  - 🍏 Rewrite data-exchange layer to push-only updates via WebSocket.
@@ -13,6 +13,7 @@ import { cmpClosedDesc, cmpPriorityThenCreated } from './sort.js';
13
13
  *
14
14
  * Source of truth is per-subscription stores providing snapshots for a given
15
15
  * client id. Central issues store fallback has been removed.
16
+ *
16
17
  * @param {{ snapshotFor?: (client_id: string) => IssueLite[], subscribe?: (fn: () => void) => () => void }} [issue_stores]
17
18
  */
18
19
  export function createListSelectors(issue_stores = undefined) {
@@ -20,6 +21,7 @@ export function createListSelectors(issue_stores = undefined) {
20
21
 
21
22
  /**
22
23
  * Get entities for a subscription id with Issues List sort (priority asc → created asc).
24
+ *
23
25
  * @param {string} client_id
24
26
  * @returns {IssueLite[]}
25
27
  */
@@ -35,6 +37,7 @@ export function createListSelectors(issue_stores = undefined) {
35
37
 
36
38
  /**
37
39
  * Get entities for a Board column with column-specific sort.
40
+ *
38
41
  * @param {string} client_id
39
42
  * @param {'ready'|'blocked'|'in_progress'|'closed'} mode
40
43
  * @returns {IssueLite[]}
@@ -58,6 +61,7 @@ export function createListSelectors(issue_stores = undefined) {
58
61
  /**
59
62
  * Get children for an epic subscribed as client id `epic:${id}`.
60
63
  * Sorted as Issues List (priority asc → created asc).
64
+ *
61
65
  * @param {string} epic_id
62
66
  * @returns {IssueLite[]}
63
67
  */
@@ -79,6 +83,7 @@ export function createListSelectors(issue_stores = undefined) {
79
83
 
80
84
  /**
81
85
  * Subscribe for re-render; triggers once per issues envelope.
86
+ *
82
87
  * @param {() => void} fn
83
88
  * @returns {() => void}
84
89
  */
@@ -5,6 +5,7 @@
5
5
  * Data layer: typed wrappers around the ws transport for mutations and
6
6
  * single-issue fetch. List reads have been removed in favor of push-only
7
7
  * stores and selectors (see docs/adr/001-push-only-lists.md).
8
+ *
8
9
  * @param {(type: MessageType, payload?: unknown) => Promise<unknown>} transport - Request/response function.
9
10
  * @returns {{ updateIssue: (input: { id: string, title?: string, acceptance?: string, notes?: string, design?: string, status?: 'open'|'in_progress'|'closed', priority?: number, assignee?: string }) => Promise<unknown> }}
10
11
  */
@@ -13,6 +14,7 @@ export function createDataLayer(transport) {
13
14
  * Update issue fields by dispatching specific mutations.
14
15
  * Supported fields: title, acceptance, notes, design, status, priority, assignee.
15
16
  * Returns the updated issue on success.
17
+ *
16
18
  * @param {{ id: string, title?: string, acceptance?: string, notes?: string, design?: string, status?: 'open'|'in_progress'|'closed', priority?: number, assignee?: string }} input
17
19
  * @returns {Promise<unknown>}
18
20
  */
package/app/data/sort.js CHANGED
@@ -9,6 +9,7 @@
9
9
 
10
10
  /**
11
11
  * Compare by priority asc, then created_at asc, then id asc.
12
+ *
12
13
  * @param {IssueLite} a
13
14
  * @param {IssueLite} b
14
15
  */
@@ -30,6 +31,7 @@ export function cmpPriorityThenCreated(a, b) {
30
31
 
31
32
  /**
32
33
  * Compare by closed_at desc, then id asc for stability.
34
+ *
33
35
  * @param {IssueLite} a
34
36
  * @param {IssueLite} b
35
37
  */
@@ -9,10 +9,9 @@ import { cmpPriorityThenCreated } from './sort.js';
9
9
  * delete messages in revision order and preserves object identity per id.
10
10
  */
11
11
 
12
- // Sort comparator is centralized in app/data/sort.js
13
-
14
12
  /**
15
13
  * Create a SubscriptionIssueStore for a given subscription id.
14
+ *
16
15
  * @param {string} id
17
16
  * @param {SubscriptionIssueStoreOptions} [options]
18
17
  * @returns {SubscriptionIssueStore}
@@ -50,6 +49,7 @@ export function createSubscriptionIssueStore(id, options = {}) {
50
49
  * - Ignore messages with revision <= last_revision (except snapshot which resets first).
51
50
  * - Preserve object identity when updating an existing item by mutating
52
51
  * fields in place rather than replacing the object reference.
52
+ *
53
53
  * @param {{ type: 'snapshot'|'upsert'|'delete', id: string, revision: number, issues?: any[], issue?: any, issue_id?: string }} msg
54
54
  */
55
55
  function applyPush(msg) {
@@ -10,9 +10,6 @@ import { subKeyOf } from './subscriptions-store.js';
10
10
  * push envelopes (snapshot/upsert/delete) per subscription id and expose
11
11
  * read-only snapshots for rendering.
12
12
  */
13
-
14
- /**
15
- */
16
13
  export function createSubscriptionIssueStores() {
17
14
  /** @type {Map<string, ReturnType<typeof createSubscriptionIssueStore>>} */
18
15
  const stores_by_id = new Map();
@@ -36,19 +33,48 @@ export function createSubscriptionIssueStores() {
36
33
  /**
37
34
  * Ensure a store exists for client_id and attach a listener that fans out
38
35
  * store-level updates to global listeners.
36
+ *
39
37
  * @param {string} client_id
40
38
  * @param {{ type: string, params?: Record<string, string|number|boolean> }} [spec]
41
39
  * @param {SubscriptionIssueStoreOptions} [options]
42
40
  */
43
41
  function register(client_id, spec, options) {
44
- key_by_id.set(client_id, spec ? subKeyOf(spec) : '');
45
- if (!stores_by_id.has(client_id)) {
42
+ const next_key = spec ? subKeyOf(spec) : '';
43
+ const prev_key = key_by_id.get(client_id) || '';
44
+ const has_store = stores_by_id.has(client_id);
45
+ // If the subscription spec changed for an existing client id, replace the
46
+ // underlying store to reset revision state and avoid ignoring a fresh
47
+ // snapshot with a lower revision (different server list).
48
+ if (has_store && prev_key && next_key && prev_key !== next_key) {
49
+ const prev_store = stores_by_id.get(client_id);
50
+ if (prev_store) {
51
+ try {
52
+ prev_store.dispose();
53
+ } catch {
54
+ // ignore
55
+ }
56
+ }
57
+ const off_prev = store_unsubs.get(client_id);
58
+ if (off_prev) {
59
+ try {
60
+ off_prev();
61
+ } catch {
62
+ // ignore
63
+ }
64
+ store_unsubs.delete(client_id);
65
+ }
66
+ const new_store = createSubscriptionIssueStore(client_id, options);
67
+ stores_by_id.set(client_id, new_store);
68
+ const off_new = new_store.subscribe(() => emit());
69
+ store_unsubs.set(client_id, off_new);
70
+ } else if (!has_store) {
46
71
  const store = createSubscriptionIssueStore(client_id, options);
47
72
  stores_by_id.set(client_id, store);
48
73
  // Fan out per-store events to global subscribers
49
74
  const off = store.subscribe(() => emit());
50
75
  store_unsubs.set(client_id, off);
51
76
  }
77
+ key_by_id.set(client_id, next_key);
52
78
  return () => unregister(client_id);
53
79
  }
54
80
 
@@ -16,6 +16,7 @@
16
16
  /**
17
17
  * Generate a stable subscription key string from a spec.
18
18
  * Mirrors server `keyOf` implementation (sorted params, URLSearchParams).
19
+ *
19
20
  * @param {SubscriptionSpec} spec
20
21
  * @returns {string}
21
22
  */
@@ -38,9 +39,10 @@ export function subKeyOf(spec) {
38
39
  * Create a list subscription store.
39
40
  *
40
41
  * Wiring:
41
- * - Use `subscribeList` to register a subscription and send the request.
42
+ * - Use `subscribeList` to register a subscription and send the request.
42
43
  *
43
44
  * Selectors are synchronous and return derived state by client id.
45
+ *
44
46
  * @param {(type: MessageType, payload?: unknown) => Promise<unknown>} send - ws send.
45
47
  */
46
48
  export function createSubscriptionStore(send) {
@@ -51,6 +53,7 @@ export function createSubscriptionStore(send) {
51
53
 
52
54
  /**
53
55
  * Apply a delta to all client ids mapped to a given key.
56
+ *
54
57
  * @param {string} key
55
58
  * @param {{ added: string[], updated: string[], removed: string[] }} delta
56
59
  */
@@ -91,6 +94,7 @@ export function createSubscriptionStore(send) {
91
94
  * Subscribe to a list spec with a client-provided id.
92
95
  * Returns an unsubscribe function.
93
96
  * Creates an empty items store immediately; server will publish deltas.
97
+ *
94
98
  * @param {string} client_id
95
99
  * @param {SubscriptionSpec} spec
96
100
  * @returns {Promise<() => Promise<void>>}
@@ -158,6 +162,7 @@ export function createSubscriptionStore(send) {
158
162
  const selectors = {
159
163
  /**
160
164
  * Get an array of item ids for a subscription.
165
+ *
161
166
  * @param {string} client_id
162
167
  * @returns {string[]}
163
168
  */
@@ -170,6 +175,7 @@ export function createSubscriptionStore(send) {
170
175
  },
171
176
  /**
172
177
  * Check if an id exists in a subscription.
178
+ *
173
179
  * @param {string} client_id
174
180
  * @param {string} id
175
181
  * @returns {boolean}
@@ -183,6 +189,7 @@ export function createSubscriptionStore(send) {
183
189
  },
184
190
  /**
185
191
  * Count items for a subscription.
192
+ *
186
193
  * @param {string} client_id
187
194
  * @returns {number}
188
195
  */
@@ -192,6 +199,7 @@ export function createSubscriptionStore(send) {
192
199
  },
193
200
  /**
194
201
  * Return a shallow object copy `{ [id]: true }` for rendering helpers.
202
+ *
195
203
  * @param {string} client_id
196
204
  * @returns {Record<string, true>}
197
205
  */
package/app/main.js CHANGED
@@ -20,6 +20,7 @@ import { createWsClient } from './ws.js';
20
20
 
21
21
  /**
22
22
  * Bootstrap the SPA shell with two panels.
23
+ *
23
24
  * @param {HTMLElement} root_element - The container element to render into.
24
25
  */
25
26
  export function bootstrap(root_element) {
@@ -415,6 +416,7 @@ export function bootstrap(root_element) {
415
416
 
416
417
  /**
417
418
  * Compute subscription spec for Issues tab based on filters.
419
+ *
418
420
  * @param {{ status?: string }} filters
419
421
  * @returns {{ type: string, params?: Record<string, string|number|boolean> }}
420
422
  */
@@ -437,6 +439,7 @@ export function bootstrap(root_element) {
437
439
  let last_issues_spec_key = null;
438
440
  /**
439
441
  * Ensure only the active tab has subscriptions; clean up previous.
442
+ *
440
443
  * @param {{ view: 'issues'|'epics'|'board', filters: any }} s
441
444
  */
442
445
  function ensureTabSubscriptions(s) {
@@ -596,6 +599,7 @@ export function bootstrap(root_element) {
596
599
 
597
600
  /**
598
601
  * Manage route visibility and list subscriptions per view.
602
+ *
599
603
  * @param {{ selected_id: string | null, view: 'issues'|'epics'|'board', filters: any }} s
600
604
  */
601
605
  const onRouteChange = (s) => {
package/app/protocol.js CHANGED
@@ -58,6 +58,7 @@ export const MESSAGE_TYPES = /** @type {const} */ ([
58
58
 
59
59
  /**
60
60
  * Generate a lexically sortable request id.
61
+ *
61
62
  * @returns {string}
62
63
  */
63
64
  export function nextId() {
@@ -68,6 +69,7 @@ export function nextId() {
68
69
 
69
70
  /**
70
71
  * Create a request envelope.
72
+ *
71
73
  * @param {MessageType} type - Message type.
72
74
  * @param {unknown} [payload] - Message payload.
73
75
  * @param {string} [id] - Optional id; generated if omitted.
@@ -79,6 +81,7 @@ export function makeRequest(type, payload, id = nextId()) {
79
81
 
80
82
  /**
81
83
  * Create a successful reply envelope for a given request.
84
+ *
82
85
  * @param {RequestEnvelope} req - Original request.
83
86
  * @param {unknown} [payload] - Reply payload.
84
87
  * @returns {ReplyEnvelope}
@@ -89,10 +92,11 @@ export function makeOk(req, payload) {
89
92
 
90
93
  /**
91
94
  * Create an error reply envelope for a given request.
95
+ *
92
96
  * @param {RequestEnvelope} req - Original request.
93
- * @param {string} code - Error code.
94
- * @param {string} message - Error message.
95
- * @param {unknown} [details] - Extra details.
97
+ * @param {string} code
98
+ * @param {string} message
99
+ * @param {unknown} [details]
96
100
  * @returns {ReplyEnvelope}
97
101
  */
98
102
  export function makeError(req, code, message, details) {
@@ -106,6 +110,7 @@ export function makeError(req, code, message, details) {
106
110
 
107
111
  /**
108
112
  * Check if a value is a plain object.
113
+ *
109
114
  * @param {unknown} value
110
115
  * @returns {value is Record<string, unknown>}
111
116
  */
@@ -115,6 +120,7 @@ function isRecord(value) {
115
120
 
116
121
  /**
117
122
  * Type guard for MessageType values.
123
+ *
118
124
  * @param {unknown} value
119
125
  * @returns {value is MessageType}
120
126
  */
@@ -127,6 +133,7 @@ export function isMessageType(value) {
127
133
 
128
134
  /**
129
135
  * Type guard for RequestEnvelope.
136
+ *
130
137
  * @param {unknown} value
131
138
  * @returns {value is RequestEnvelope}
132
139
  */
@@ -143,6 +150,7 @@ export function isRequest(value) {
143
150
 
144
151
  /**
145
152
  * Type guard for ReplyEnvelope.
153
+ *
146
154
  * @param {unknown} value
147
155
  * @returns {value is ReplyEnvelope}
148
156
  */
@@ -173,6 +181,7 @@ export function isReply(value) {
173
181
  /**
174
182
  * Normalize and validate an incoming JSON value as a RequestEnvelope.
175
183
  * Throws a user-friendly error if invalid.
184
+ *
176
185
  * @param {unknown} json
177
186
  * @returns {RequestEnvelope}
178
187
  */
@@ -185,6 +194,7 @@ export function decodeRequest(json) {
185
194
 
186
195
  /**
187
196
  * Normalize and validate an incoming JSON value as a ReplyEnvelope.
197
+ *
188
198
  * @param {unknown} json
189
199
  * @returns {ReplyEnvelope}
190
200
  */
package/app/router.js CHANGED
@@ -8,6 +8,7 @@ import { issueHashFor } from './utils/issue-url.js';
8
8
  * Parse an application hash and extract the selected issue id.
9
9
  * Supports canonical form "#/(issues|epics|board)?issue=<id>" and legacy
10
10
  * "#/issue/<id>" which we will rewrite to the canonical form.
11
+ *
11
12
  * @param {string} hash
12
13
  * @returns {string | null}
13
14
  */
@@ -31,6 +32,7 @@ export function parseHash(hash) {
31
32
 
32
33
  /**
33
34
  * Parse the current view from hash.
35
+ *
34
36
  * @param {string} hash
35
37
  * @returns {'issues'|'epics'|'board'}
36
38
  */
@@ -95,6 +97,7 @@ export function createHashRouter(store) {
95
97
  },
96
98
  /**
97
99
  * Navigate to a top-level view.
100
+ *
98
101
  * @param {'issues'|'epics'|'board'} view
99
102
  */
100
103
  /**
package/app/state.js CHANGED
@@ -28,6 +28,7 @@
28
28
 
29
29
  /**
30
30
  * Create a simple store for application state.
31
+ *
31
32
  * @param {Partial<AppState>} [initial]
32
33
  * @returns {{ getState: () => AppState, setState: (patch: { selected_id?: string | null, filters?: Partial<Filters> }) => void, subscribe: (fn: (s: AppState) => void) => () => void }}
33
34
  */
@@ -71,6 +72,7 @@ export function createStore(initial = {}) {
71
72
  },
72
73
  /**
73
74
  * Update state. Nested filters can be partial.
75
+ *
74
76
  * @param {{ selected_id?: string | null, filters?: Partial<Filters>, board?: Partial<BoardState> }} patch
75
77
  */
76
78
  setState(patch) {
@@ -5,6 +5,7 @@ import { issueDisplayId } from './issue-id.js';
5
5
  * Looks like the current inline ID (monospace `#123`) but acts as a button
6
6
  * that copies the full, prefixed ID (e.g., `UI-123`) when activated.
7
7
  * Shows transient "Copied" feedback and then restores the ID.
8
+ *
8
9
  * @param {string} id - Full issue id including the prefix (e.g., "UI-123").
9
10
  * @param {{ class_name?: string, duration_ms?: number }} [opts]
10
11
  * @returns {HTMLButtonElement}
@@ -25,7 +26,7 @@ export function createIssueIdRenderer(id, opts) {
25
26
  const label = issueDisplayId(id);
26
27
  btn.textContent = label;
27
28
 
28
- /** Copy handler with feedback */
29
+ /** Copy handler with feedback. */
29
30
  async function doCopy() {
30
31
  // Prevent accidental row navigation and parent handlers
31
32
  // (click/key handlers call this inside an event context)
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Format a beads issue id as a user-facing display string `#${n}`.
3
3
  * Extracts the trailing numeric portion of the id and prefixes with '#'.
4
+ *
4
5
  * @param {string | null | undefined} id
5
6
  * @returns {string}
6
7
  */
@@ -1,11 +1,13 @@
1
1
  /**
2
2
  * Known issue types in canonical order for dropdowns.
3
+ *
3
4
  * @type {Array<'bug'|'feature'|'task'|'epic'|'chore'>}
4
5
  */
5
6
  export const ISSUE_TYPES = ['bug', 'feature', 'task', 'epic', 'chore'];
6
7
 
7
8
  /**
8
9
  * Return a human-friendly label for an issue type.
10
+ *
9
11
  * @param {string | null | undefined} type
10
12
  * @returns {string}
11
13
  */
@@ -1,5 +1,6 @@
1
1
  /**
2
2
  * Build a canonical issue hash that retains the view.
3
+ *
3
4
  * @param {'issues'|'epics'|'board'} view
4
5
  * @param {string} id
5
6
  */
@@ -1,5 +1,4 @@
1
1
  import DOMPurify from 'dompurify';
2
- import { html } from 'lit-html';
3
2
  import { unsafeHTML } from 'lit-html/directives/unsafe-html.js';
4
3
  import { marked } from 'marked';
5
4
 
@@ -7,16 +6,11 @@ import { marked } from 'marked';
7
6
  * Render Markdown safely as HTML using marked and DOMPurify.
8
7
  * Returns a lit-html TemplateResult via the unsafeHTML directive so it can be
9
8
  * embedded directly in templates.
10
- * @function renderMarkdown
11
- * @param {string} src Markdown source text
12
- * @returns {import('lit-html').TemplateResult}
9
+ *
10
+ * @param {string} markdown - Markdown source text
13
11
  */
14
- export function renderMarkdown(src) {
15
- /** @type {string} */
16
- const markdown = String(src || '');
17
- /** @type {string} */
12
+ export function renderMarkdown(markdown) {
18
13
  const parsed = /** @type {string} */ (marked.parse(markdown));
19
- /** @type {string} */
20
14
  const html_string = DOMPurify.sanitize(parsed);
21
- return html`${unsafeHTML(html_string)}`;
15
+ return unsafeHTML(html_string);
22
16
  }
@@ -2,6 +2,7 @@ import { priority_levels } from './priority.js';
2
2
 
3
3
  /**
4
4
  * Create a colored badge for a priority value (0..4).
5
+ *
5
6
  * @param {number | null | undefined} priority
6
7
  * @returns {HTMLSpanElement}
7
8
  */
@@ -1,5 +1,6 @@
1
1
  /**
2
2
  * Create a colored badge for a status value.
3
+ *
3
4
  * @param {string | null | undefined} status - 'open' | 'in_progress' | 'closed'
4
5
  * @returns {HTMLSpanElement}
5
6
  */
@@ -1,11 +1,13 @@
1
1
  /**
2
2
  * Known status values in canonical order.
3
+ *
3
4
  * @type {Array<'open'|'in_progress'|'closed'>}
4
5
  */
5
6
  export const STATUSES = ['open', 'in_progress', 'closed'];
6
7
 
7
8
  /**
8
9
  * Map canonical status to display label.
10
+ *
9
11
  * @param {string | null | undefined} status
10
12
  * @returns {string}
11
13
  */
@@ -1,5 +1,6 @@
1
1
  /**
2
2
  * Show a transient global toast message anchored to the viewport.
3
+ *
3
4
  * @param {string} text - Message text.
4
5
  * @param {'info'|'success'|'error'} [variant] - Visual variant.
5
6
  * @param {number} [duration_ms] - Auto-dismiss delay in milliseconds.
@@ -1,5 +1,6 @@
1
1
  /**
2
2
  * Create a compact, colored badge for an issue type.
3
+ *
3
4
  * @param {string | undefined | null} issue_type - One of: bug, feature, task, epic, chore
4
5
  * @returns {HTMLSpanElement}
5
6
  */
@@ -23,8 +23,9 @@ import { createTypeBadge } from '../utils/type-badge.js';
23
23
  * Push-only: derives items from per-subscription stores.
24
24
  *
25
25
  * Sorting rules:
26
- * - Ready/Blocked/In progress: priority asc, then created_at asc
27
- * - Closed: closed_at desc
26
+ * - Ready/Blocked/In progress: priority asc, then created_at asc.
27
+ * - Closed: closed_at desc.
28
+ *
28
29
  * @param {HTMLElement} mount_element
29
30
  * @param {unknown} _data - Unused (legacy param retained for call-compat)
30
31
  * @param {(id: string) => void} gotoIssue - Navigate to issue detail.
@@ -58,6 +59,7 @@ export function createBoardView(
58
59
  * Closed column filter mode.
59
60
  * 'today' → items with closed_at since local day start
60
61
  * '3' → last 3 days; '7' → last 7 days
62
+ *
61
63
  * @type {'today'|'3'|'7'}
62
64
  */
63
65
  let closed_filter_mode = 'today';
@@ -165,10 +167,10 @@ export function createBoardView(
165
167
 
166
168
  /**
167
169
  * Enhance rendered board with a11y and keyboard navigation.
168
- * - Roving tabindex per column (first card tabbable)
169
- * - ArrowUp/ArrowDown within column
170
- * - ArrowLeft/ArrowRight to adjacent non-empty column (focus top card)
171
- * - Enter/Space to open details for focused card
170
+ * - Roving tabindex per column (first card tabbable).
171
+ * - ArrowUp/ArrowDown within column.
172
+ * - ArrowLeft/ArrowRight to adjacent non-empty column (focus top card).
173
+ * - Enter/Space to open details for focused card.
172
174
  */
173
175
  function postRenderEnhance() {
174
176
  try {
@@ -44,6 +44,7 @@ function defaultNavigateFn(hash) {
44
44
 
45
45
  /**
46
46
  * Create the Issue Detail view.
47
+ *
47
48
  * @param {HTMLElement} mount_element - Element to render into.
48
49
  * @param {(type: string, payload?: unknown) => Promise<unknown>} sendFn - RPC transport.
49
50
  * @param {(hash: string) => void} [navigateFn] - Navigation function; defaults to setting location.hash.
@@ -1071,6 +1072,7 @@ export function createDetailView(
1071
1072
 
1072
1073
  /**
1073
1074
  * Create a click handler for the remove button of a dependency row.
1075
+ *
1074
1076
  * @param {string} did
1075
1077
  * @param {'Dependencies'|'Dependents'} title
1076
1078
  * @returns {(ev: Event) => Promise<void>}
@@ -1114,6 +1116,7 @@ export function createDetailView(
1114
1116
 
1115
1117
  /**
1116
1118
  * Create a click handler for the Add button in a dependency section.
1119
+ *
1117
1120
  * @param {Dependency[]} items
1118
1121
  * @param {'Dependencies'|'Dependents'} title
1119
1122
  * @returns {(ev: Event) => Promise<void>}
@@ -9,11 +9,12 @@ import { createIssueRowRenderer } from './issue-row.js';
9
9
 
10
10
  /**
11
11
  * Epics view (push-only):
12
- * - Derives epic groups from the local issues store (no RPC reads)
13
- * - Subscribes to `tab:epics` for top-level membership
14
- * - On expand, subscribes to `detail:{id}` (issue-detail) for the epic
15
- * - Renders children from the epic detail's `dependents` list
16
- * - Provides inline edits via mutations; UI re-renders on push
12
+ * - Derives epic groups from the local issues store (no RPC reads).
13
+ * - Subscribes to `tab:epics` for top-level membership.
14
+ * - On expand, subscribes to `detail:{id}` (issue-detail) for the epic.
15
+ * - Renders children from the epic detail's `dependents` list.
16
+ * - Provides inline edits via mutations; UI re-renders on push.
17
+ *
17
18
  * @param {HTMLElement} mount_element
18
19
  * @param {{ updateIssue: (input: any) => Promise<any> }} data
19
20
  * @param {(id: string) => void} goto_issue - Navigate to issue detail.
@@ -10,6 +10,7 @@ import { createIssueIdRenderer } from '../utils/issue-id-renderer.js';
10
10
 
11
11
  /**
12
12
  * Create and manage the Issue Details dialog.
13
+ *
13
14
  * @param {HTMLElement} mount_element - Container to attach the <dialog> to (e.g., #detail-panel)
14
15
  * @param {Store} store - Read-only access to app state
15
16
  * @param {() => void} onClose - Called when dialog requests close (backdrop/esc/button)
@@ -12,6 +12,7 @@ import { createTypeBadge } from '../utils/type-badge.js';
12
12
  /**
13
13
  * Create a reusable issue row renderer used by list and epics views.
14
14
  * Handles inline editing for title/assignee and selects for status/priority.
15
+ *
15
16
  * @param {{
16
17
  * navigate: (id: string) => void,
17
18
  * onUpdate: (id: string, patch: { title?: string, assignee?: string, status?: 'open'|'in_progress'|'closed', priority?: number }) => Promise<void>,
package/app/views/list.js CHANGED
@@ -15,6 +15,7 @@ import { createIssueRowRenderer } from './issue-row.js';
15
15
 
16
16
  /**
17
17
  * Create the Issues List view.
18
+ *
18
19
  * @param {HTMLElement} mount_element - Element to render into.
19
20
  * @param {(type: string, payload?: unknown) => Promise<unknown>} sendFn - RPC transport.
20
21
  * @param {(hash: string) => void} [navigate_fn] - Navigation function (defaults to setting location.hash).
@@ -25,6 +26,7 @@ import { createIssueRowRenderer } from './issue-row.js';
25
26
  */
26
27
  /**
27
28
  * Create the Issues List view.
29
+ *
28
30
  * @param {HTMLElement} mount_element
29
31
  * @param {(type: string, payload?: unknown) => Promise<unknown>} sendFn
30
32
  * @param {(hash: string) => void} [navigateFn]
@@ -104,6 +106,7 @@ export function createListView(
104
106
 
105
107
  /**
106
108
  * Event: type select change.
109
+ *
107
110
  * @param {Event} ev
108
111
  */
109
112
  const onTypeChange = (ev) => {
@@ -237,6 +240,7 @@ export function createListView(
237
240
 
238
241
  /**
239
242
  * Update minimal fields inline via ws mutations and refresh that row's data.
243
+ *
240
244
  * @param {string} id
241
245
  * @param {{ [k: string]: any }} patch
242
246
  */
package/app/views/nav.js CHANGED
@@ -2,6 +2,7 @@ import { html, render } from 'lit-html';
2
2
 
3
3
  /**
4
4
  * Render the top navigation with three tabs and handle route changes.
5
+ *
5
6
  * @param {HTMLElement} mount_element
6
7
  * @param {{ getState: () => any, subscribe: (fn: (s: any) => void) => () => void }} store
7
8
  * @param {{ gotoView: (v: 'issues'|'epics'|'board') => void }} router
@@ -3,6 +3,7 @@ import { priority_levels } from '../utils/priority.js';
3
3
 
4
4
  /**
5
5
  * Create and manage the New Issue dialog (native <dialog>).
6
+ *
6
7
  * @param {HTMLElement} mount_element - Container to attach dialog (e.g., main#app)
7
8
  * @param {(type: import('../protocol.js').MessageType, payload?: unknown) => Promise<unknown>} sendFn - Transport function
8
9
  * @param {{ gotoIssue: (id: string) => void }} router - Router for opening details after create
@@ -184,6 +185,7 @@ export function createNewIssueDialog(mount_element, sendFn, router, store) {
184
185
 
185
186
  /**
186
187
  * Extract numeric suffix from an id like "UI-123"; return -1 when absent.
188
+ *
187
189
  * @param {string} id
188
190
  */
189
191
  function idNumeric(id) {
@@ -193,6 +195,7 @@ export function createNewIssueDialog(mount_element, sendFn, router, store) {
193
195
 
194
196
  /**
195
197
  * Submit handler: validate, create, then open the created issue details.
198
+ *
196
199
  * @returns {Promise<void>}
197
200
  */
198
201
  async function createNow() {
package/app/ws.js CHANGED
@@ -1,4 +1,3 @@
1
- /* global Console */
2
1
  /**
3
2
  * @import { MessageType } from './protocol.js'
4
3
  */
@@ -27,6 +26,7 @@ import { MESSAGE_TYPES, makeRequest, nextId } from './protocol.js';
27
26
 
28
27
  /**
29
28
  * Create a WebSocket client with auto-reconnect and message correlation.
29
+ *
30
30
  * @param {ClientOptions} [options]
31
31
  */
32
32
  export function createWsClient(options = {}) {
@@ -211,6 +211,7 @@ export function createWsClient(options = {}) {
211
211
  return {
212
212
  /**
213
213
  * Send a request and await its correlated reply payload.
214
+ *
214
215
  * @param {MessageType} type
215
216
  * @param {unknown} [payload]
216
217
  * @returns {Promise<any>}
@@ -233,6 +234,7 @@ export function createWsClient(options = {}) {
233
234
  /**
234
235
  * Register a handler for a server-initiated event type.
235
236
  * Returns an unsubscribe function.
237
+ *
236
238
  * @param {MessageType} type
237
239
  * @param {(payload: any) => void} handler
238
240
  * @returns {() => void}
@@ -249,6 +251,7 @@ export function createWsClient(options = {}) {
249
251
  },
250
252
  /**
251
253
  * Subscribe to connection state changes.
254
+ *
252
255
  * @param {(state: ConnectionState) => void} handler
253
256
  * @returns {() => void}
254
257
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "beads-ui",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Local‑first UI for Beads — a fast issue tracker for your coding agent.",
5
5
  "homepage": "https://github.com/mantoni/beads-ui",
6
6
  "type": "module",
@@ -32,9 +32,8 @@
32
32
  "@types/ws": "^8.18.1",
33
33
  "eslint": "^9.11.0",
34
34
  "eslint-plugin-import": "^2.29.1",
35
- "eslint-plugin-jsdoc": "^48.10.2",
35
+ "eslint-plugin-jsdoc": "^61.1.9",
36
36
  "eslint-plugin-n": "^17.9.0",
37
- "eslint-plugin-promise": "^6.1.1",
38
37
  "globals": "^16.4.0",
39
38
  "jsdom": "^27.0.1",
40
39
  "prettier": "^3.3.3",
package/server/app.js CHANGED
@@ -6,6 +6,7 @@ import path from 'node:path';
6
6
 
7
7
  /**
8
8
  * Create and configure the Express application.
9
+ *
9
10
  * @param {{ host: string, port: number, env: string, app_dir: string, root_dir: string }} config - Server configuration.
10
11
  * @returns {Express} Configured Express app instance.
11
12
  */
@@ -28,6 +29,7 @@ export function createApp(config) {
28
29
  /**
29
30
  * On-demand bundle for the browser using esbuild.
30
31
  * Note: esbuild is loaded lazily so tests don't require it to be installed.
32
+ *
31
33
  * @param {Request} _req
32
34
  * @param {Response} res
33
35
  */
package/server/bd.js CHANGED
@@ -3,6 +3,7 @@ import { resolveDbPath } from './db.js';
3
3
 
4
4
  /**
5
5
  * Resolve the bd executable path.
6
+ *
6
7
  * @returns {string}
7
8
  */
8
9
  export function getBdBin() {
@@ -16,6 +17,7 @@ export function getBdBin() {
16
17
  /**
17
18
  * Run the `bd` CLI with provided arguments.
18
19
  * Shell is not used to avoid injection; args must be pre-split.
20
+ *
19
21
  * @param {string[]} args - Arguments to pass (e.g., ["list", "--json"]).
20
22
  * @param {{ cwd?: string, env?: Record<string, string | undefined>, timeout_ms?: number }} [options]
21
23
  * @returns {Promise<{ code: number, stdout: string, stderr: string }>}
@@ -88,6 +90,7 @@ export function runBd(args, options = {}) {
88
90
 
89
91
  /**
90
92
  * Run `bd` and parse JSON from stdout if exit code is 0.
93
+ *
91
94
  * @param {string[]} args - Must include flags that cause JSON to be printed (e.g., `--json`).
92
95
  * @param {{ cwd?: string, env?: Record<string, string | undefined>, timeout_ms?: number }} [options]
93
96
  * @returns {Promise<{ code: number, stdoutJson?: unknown, stderr?: string }>}
@@ -109,6 +112,7 @@ export async function runBdJson(args, options = {}) {
109
112
 
110
113
  /**
111
114
  * Add a resolved "--db <path>" pair to args when none present.
115
+ *
112
116
  * @param {string[]} args
113
117
  * @param {string} cwd
114
118
  * @param {Record<string, string | undefined>} env
@@ -13,6 +13,7 @@ import { openUrl, waitForServer } from './open.js';
13
13
  * Handle `start` command. Idempotent when already running.
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
  * @returns {Promise<number>} Exit code (0 on success)
17
18
  */
18
19
  /**
@@ -52,6 +53,7 @@ export async function handleStart(options) {
52
53
  * Handle `stop` command.
53
54
  * - Sends SIGTERM and waits for exit (with SIGKILL fallback), removes PID file.
54
55
  * - Returns 2 if not running.
56
+ *
55
57
  * @returns {Promise<number>} Exit code
56
58
  */
57
59
  export async function handleStop() {
@@ -78,12 +80,14 @@ export async function handleStop() {
78
80
 
79
81
  /**
80
82
  * Handle `restart` command: stop (ignore not-running) then start.
83
+ *
81
84
  * @returns {Promise<number>} Exit code (0 on success)
82
85
  */
83
86
  /**
84
87
  * Handle `restart` command: stop (ignore not-running) then start.
85
88
  * Accepts the same options as `handleStart` and passes them through,
86
89
  * so restart only opens a browser when `no_open` is explicitly false.
90
+ *
87
91
  * @param {{ no_open?: boolean }} [options]
88
92
  * @returns {Promise<number>}
89
93
  */
@@ -13,6 +13,7 @@ import { resolveDbPath } from '../db.js';
13
13
  * Resolve the runtime directory used for PID and log files.
14
14
  * Prefers `BDUI_RUNTIME_DIR`, then `$XDG_RUNTIME_DIR/beads-ui`,
15
15
  * and finally `os.tmpdir()/beads-ui`.
16
+ *
16
17
  * @returns {string}
17
18
  */
18
19
  export function getRuntimeDir() {
@@ -31,6 +32,7 @@ export function getRuntimeDir() {
31
32
 
32
33
  /**
33
34
  * Ensure a directory exists with safe permissions and return its path.
35
+ *
34
36
  * @param {string} dir_path
35
37
  * @returns {string}
36
38
  */
@@ -61,6 +63,7 @@ export function getLogFilePath() {
61
63
 
62
64
  /**
63
65
  * Read PID from the PID file if present.
66
+ *
64
67
  * @returns {number | null}
65
68
  */
66
69
  export function readPidFile() {
@@ -100,6 +103,7 @@ export function removePidFile() {
100
103
 
101
104
  /**
102
105
  * Check whether a process is running.
106
+ *
103
107
  * @param {number} pid
104
108
  * @returns {boolean}
105
109
  */
@@ -122,6 +126,7 @@ export function isProcessRunning(pid) {
122
126
 
123
127
  /**
124
128
  * Compute the absolute path to the server entry file.
129
+ *
125
130
  * @returns {string}
126
131
  */
127
132
  export function getServerEntryPath() {
@@ -134,6 +139,7 @@ export function getServerEntryPath() {
134
139
  /**
135
140
  * Spawn the server as a detached daemon, redirecting stdio to the log file.
136
141
  * Writes the PID file upon success.
142
+ *
137
143
  * @returns {{ pid: number } | null} Returns child PID on success; null on failure.
138
144
  */
139
145
  export function startDaemon() {
@@ -183,6 +189,7 @@ export function startDaemon() {
183
189
 
184
190
  /**
185
191
  * Send SIGTERM then (optionally) SIGKILL to stop a process and wait for exit.
192
+ *
186
193
  * @param {number} pid
187
194
  * @param {number} timeout_ms
188
195
  * @returns {Promise<boolean>} Resolves true if the process is gone.
@@ -3,6 +3,7 @@ import { printUsage } from './usage.js';
3
3
 
4
4
  /**
5
5
  * Parse argv into a command token and flags.
6
+ *
6
7
  * @param {string[]} args
7
8
  * @returns {{ command: string | null, flags: string[] }}
8
9
  */
@@ -41,6 +42,7 @@ export function parseArgs(args) {
41
42
  /**
42
43
  * CLI main entry. Returns an exit code and prints usage on `--help` or errors.
43
44
  * No side effects beyond invoking stub handlers.
45
+ *
44
46
  * @param {string[]} args
45
47
  * @returns {Promise<number>}
46
48
  */
@@ -3,6 +3,7 @@ import http from 'node:http';
3
3
 
4
4
  /**
5
5
  * Compute a platform-specific command to open a URL in the default browser.
6
+ *
6
7
  * @param {string} url
7
8
  * @param {string} platform
8
9
  * @returns {{ cmd: string, args: string[] }}
@@ -21,6 +22,7 @@ export function computeOpenCommand(url, platform) {
21
22
 
22
23
  /**
23
24
  * Open the given URL in the default browser. Best-effort; resolves true on spawn success.
25
+ *
24
26
  * @param {string} url
25
27
  * @returns {Promise<boolean>}
26
28
  */
@@ -41,6 +43,7 @@ export async function openUrl(url) {
41
43
  /**
42
44
  * Wait until the server at the URL accepts a connection, with a brief retry.
43
45
  * Does not throw; returns when either a connection was accepted or timeout elapsed.
46
+ *
44
47
  * @param {string} url
45
48
  * @param {number} total_timeout_ms
46
49
  * @returns {Promise<void>}
@@ -1,5 +1,6 @@
1
1
  /**
2
2
  * Print CLI usage to a stream-like target.
3
+ *
3
4
  * @param {{ write: (chunk: string) => any }} out_stream
4
5
  */
5
6
  export function printUsage(out_stream) {
package/server/config.js CHANGED
@@ -6,8 +6,9 @@ import { fileURLToPath } from 'node:url';
6
6
  * Notes:
7
7
  * - `app_dir` is resolved relative to the installed package location.
8
8
  * - `root_dir` represents the directory where the process was invoked
9
- * (i.e., the current working directory) so DB resolution follows the
10
- * caller's context rather than the install location.
9
+ * (i.e., the current working directory) so DB resolution follows the
10
+ * caller's context rather than the install location.
11
+ *
11
12
  * @returns {{ host: string, port: number, env: string, app_dir: string, root_dir: string, url: string }}
12
13
  */
13
14
  export function getConfig() {
package/server/db.js CHANGED
@@ -11,6 +11,7 @@ import path from 'node:path';
11
11
  *
12
12
  * Returns a normalized absolute path and a `source` indicator. Existence is
13
13
  * returned via the `exists` boolean.
14
+ *
14
15
  * @param {{ cwd?: string, env?: Record<string, string | undefined>, explicit_db?: string }} [options]
15
16
  * @returns {{ path: string, source: 'flag'|'env'|'nearest'|'home-default', exists: boolean }}
16
17
  */
@@ -48,6 +49,7 @@ export function resolveDbPath(options = {}) {
48
49
  /**
49
50
  * Find nearest .beads/*.db by walking up from start.
50
51
  * First alphabetical .db.
52
+ *
51
53
  * @param {string} start
52
54
  * @returns {string | null}
53
55
  */
@@ -79,6 +81,7 @@ export function findNearestBeadsDb(start) {
79
81
 
80
82
  /**
81
83
  * Resolve possibly relative `p` against `cwd` to an absolute filesystem path.
84
+ *
82
85
  * @param {string} p
83
86
  * @param {string} cwd
84
87
  */
@@ -3,6 +3,7 @@ import { runBdJson } from './bd.js';
3
3
  /**
4
4
  * Build concrete `bd` CLI args for a subscription type + params.
5
5
  * Always includes `--json` for parseable output.
6
+ *
6
7
  * @param {{ type: string, params?: Record<string, string | number | boolean> }} spec
7
8
  * @returns {string[]}
8
9
  */
@@ -43,9 +44,10 @@ export function mapSubscriptionToBdArgs(spec) {
43
44
 
44
45
  /**
45
46
  * Normalize bd list output to minimal Issue shape used by the registry.
46
- * - Ensures `id` is a string
47
- * - Coerces timestamps to numbers
48
- * - `closed_at` defaults to null when missing or invalid
47
+ * - Ensures `id` is a string.
48
+ * - Coerces timestamps to numbers.
49
+ * - `closed_at` defaults to null when missing or invalid.
50
+ *
49
51
  * @param {unknown} value
50
52
  * @returns {Array<{ id: string, created_at: number, updated_at: number, closed_at: number | null } & Record<string, unknown>>}
51
53
  */
@@ -95,6 +97,7 @@ export function normalizeIssueList(value) {
95
97
  /**
96
98
  * Execute the mapped `bd` command for a subscription spec and return normalized items.
97
99
  * Errors do not throw; they are surfaced as a structured object.
100
+ *
98
101
  * @param {{ type: string, params?: Record<string, string | number | boolean> }} spec
99
102
  * @returns {Promise<FetchListResultSuccess | FetchListResultFailure>}
100
103
  */
@@ -171,6 +174,7 @@ export async function fetchListForSubscription(spec) {
171
174
 
172
175
  /**
173
176
  * Create a `bad_request` error object.
177
+ *
174
178
  * @param {string} message
175
179
  */
176
180
  function badRequest(message) {
@@ -182,6 +186,7 @@ function badRequest(message) {
182
186
 
183
187
  /**
184
188
  * Normalize arbitrary thrown values to a structured error object.
189
+ *
185
190
  * @param {unknown} err
186
191
  * @returns {FetchListResultFailure['error']}
187
192
  */
@@ -199,6 +204,7 @@ function toErrorObject(err) {
199
204
  /**
200
205
  * Parse a bd timestamp string to epoch ms using Date.parse.
201
206
  * Falls back to numeric coercion when parsing fails.
207
+ *
202
208
  * @param {unknown} v
203
209
  * @returns {number}
204
210
  */
@@ -36,6 +36,7 @@
36
36
 
37
37
  /**
38
38
  * Create a new, empty entry object.
39
+ *
39
40
  * @returns {Entry}
40
41
  */
41
42
  function createEntry() {
@@ -49,6 +50,7 @@ function createEntry() {
49
50
 
50
51
  /**
51
52
  * Generate a stable subscription key string from a spec. Sorts params keys.
53
+ *
52
54
  * @param {SubscriptionSpec} spec
53
55
  * @returns {string}
54
56
  */
@@ -69,6 +71,7 @@ export function keyOf(spec) {
69
71
 
70
72
  /**
71
73
  * Compute a delta between previous and next item maps.
74
+ *
72
75
  * @param {Map<string, ItemMeta>} prev
73
76
  * @param {Map<string, ItemMeta>} next
74
77
  * @returns {{ added: string[], updated: string[], removed: string[] }}
@@ -101,6 +104,7 @@ export function computeDelta(prev, next) {
101
104
 
102
105
  /**
103
106
  * Normalize array of issue-like objects into an itemsById map.
107
+ *
104
108
  * @param {Array<{ id: string, updated_at: number, closed_at?: number|null }>} items
105
109
  * @returns {Map<string, ItemMeta>}
106
110
  */
@@ -136,6 +140,7 @@ export class SubscriptionRegistry {
136
140
 
137
141
  /**
138
142
  * Get an entry by key, or null if missing.
143
+ *
139
144
  * @param {string} key
140
145
  * @returns {Entry | null}
141
146
  */
@@ -145,6 +150,7 @@ export class SubscriptionRegistry {
145
150
 
146
151
  /**
147
152
  * Ensure an entry exists for a spec; returns the key and entry.
153
+ *
148
154
  * @param {SubscriptionSpec} spec
149
155
  * @returns {{ key: string, entry: Entry }}
150
156
  */
@@ -160,6 +166,7 @@ export class SubscriptionRegistry {
160
166
 
161
167
  /**
162
168
  * Attach a subscriber to a spec. Creates the entry if missing.
169
+ *
163
170
  * @param {SubscriptionSpec} spec
164
171
  * @param {WebSocket} ws
165
172
  * @returns {{ key: string, subscribed: true }}
@@ -173,6 +180,7 @@ export class SubscriptionRegistry {
173
180
  /**
174
181
  * Detach a subscriber from the spec. Keeps entry even if empty; eviction
175
182
  * is handled by `onDisconnect` sweep.
183
+ *
176
184
  * @param {SubscriptionSpec} spec
177
185
  * @param {WebSocket} ws
178
186
  * @returns {boolean} true when the subscriber was removed
@@ -189,6 +197,7 @@ export class SubscriptionRegistry {
189
197
  /**
190
198
  * On socket disconnect, remove it from all subscriber sets and evict any
191
199
  * entries that become empty as a result of this sweep.
200
+ *
192
201
  * @param {WebSocket} ws
193
202
  */
194
203
  onDisconnect(ws) {
@@ -207,6 +216,7 @@ export class SubscriptionRegistry {
207
216
 
208
217
  /**
209
218
  * Serialize a function against a key so only one runs at a time per key.
219
+ *
210
220
  * @template T
211
221
  * @param {string} key
212
222
  * @param {() => Promise<T>} fn
@@ -243,6 +253,7 @@ export class SubscriptionRegistry {
243
253
 
244
254
  /**
245
255
  * Replace items for a key and compute the delta, storing the new map.
256
+ *
246
257
  * @param {string} key
247
258
  * @param {Map<string, ItemMeta>} next_map
248
259
  * @returns {{ added: string[], updated: string[], removed: string[] }}
@@ -261,6 +272,7 @@ export class SubscriptionRegistry {
261
272
 
262
273
  /**
263
274
  * Convenience: update items from an array of objects with id/updated_at/closed_at.
275
+ *
264
276
  * @param {string} key
265
277
  * @param {Array<{ id: string, updated_at: number, closed_at?: number|null }>} items
266
278
  * @returns {{ added: string[], updated: string[], removed: string[] }}
@@ -6,6 +6,7 @@
6
6
 
7
7
  /**
8
8
  * Known subscription types supported by the server.
9
+ *
9
10
  * @type {Set<string>}
10
11
  */
11
12
  const SUBSCRIPTION_TYPES = new Set([
@@ -20,6 +21,7 @@ const SUBSCRIPTION_TYPES = new Set([
20
21
 
21
22
  /**
22
23
  * Validate a subscribe-list payload and normalize to a SubscriptionSpec.
24
+ *
23
25
  * @param {unknown} payload
24
26
  * @returns {{ ok: true, id: string, spec: { type: string, params?: Record<string, string|number|boolean> } } | { ok: false, code: 'bad_request', message: string }}
25
27
  */
package/server/watcher.js CHANGED
@@ -5,6 +5,7 @@ import { resolveDbPath } from './db.js';
5
5
  /**
6
6
  * Watch the resolved beads SQLite DB file and invoke a callback after a debounce window.
7
7
  * The DB path is resolved following beads precedence and can be overridden via options.
8
+ *
8
9
  * @param {string} root_dir - Project root directory (starting point for resolution).
9
10
  * @param {() => void} onChange - Called when changes are detected.
10
11
  * @param {{ debounce_ms?: number, explicit_db?: string }} [options]
@@ -33,6 +34,7 @@ export function watchDb(root_dir, onChange, options = {}) {
33
34
 
34
35
  /**
35
36
  * Attach a watcher to the directory containing the resolved DB path.
37
+ *
36
38
  * @param {string} base_dir
37
39
  * @param {string | undefined} explicit_db
38
40
  */
@@ -84,6 +86,7 @@ export function watchDb(root_dir, onChange, options = {}) {
84
86
  },
85
87
  /**
86
88
  * Re-resolve and reattach watcher when root_dir or explicit_db changes.
89
+ *
87
90
  * @param {{ root_dir?: string, explicit_db?: string }} [opts]
88
91
  */
89
92
  rebind(opts = {}) {
package/server/ws.js CHANGED
@@ -40,6 +40,7 @@ let MUTATION_GATE = null;
40
40
  * suppressed during the window.
41
41
  *
42
42
  * Fire-and-forget; callers should not await this.
43
+ *
43
44
  * @param {number} [timeout_ms]
44
45
  */
45
46
  function triggerMutationRefreshOnce(timeout_ms = 500) {
@@ -95,6 +96,7 @@ function triggerMutationRefreshOnce(timeout_ms = 500) {
95
96
 
96
97
  /**
97
98
  * Collect unique active list subscription specs across all connected clients.
99
+ *
98
100
  * @returns {Array<{ type: string, params?: Record<string,string|number|boolean> }>}
99
101
  */
100
102
  function collectActiveListSpecs() {
@@ -181,6 +183,7 @@ let CURRENT_WSS = null;
181
183
 
182
184
  /**
183
185
  * Get or initialize the subscription state for a socket.
186
+ *
184
187
  * @param {WebSocket} ws
185
188
  * @returns {any}
186
189
  */
@@ -199,6 +202,7 @@ function ensureSubs(ws) {
199
202
 
200
203
  /**
201
204
  * Get next monotonically increasing revision for a subscription key on this connection.
205
+ *
202
206
  * @param {WebSocket} ws
203
207
  * @param {string} key
204
208
  */
@@ -306,6 +310,7 @@ function emitSubscriptionDelete(ws, client_id, key, issue_id) {
306
310
  /**
307
311
  * Refresh a subscription spec: fetch via adapter, apply to registry and emit
308
312
  * per-subscription full-issue envelopes to subscribers. Serialized per key.
313
+ *
309
314
  * @param {{ type: string, params?: Record<string, string|number|boolean> }} spec
310
315
  */
311
316
  async function refreshAndPublish(spec) {
@@ -362,6 +367,7 @@ async function refreshAndPublish(spec) {
362
367
 
363
368
  /**
364
369
  * Apply pre-diff filtering for closed-issues lists based on spec.params.since (epoch ms).
370
+ *
365
371
  * @param {{ type: string, params?: Record<string, string|number|boolean> }} spec
366
372
  * @param {Array<{ id: string, updated_at: number, closed_at: number | null } & Record<string, unknown>>} items
367
373
  */
@@ -387,6 +393,7 @@ function applyClosedIssuesFilter(spec, items) {
387
393
 
388
394
  /**
389
395
  * Attach a WebSocket server to an existing HTTP server.
396
+ *
390
397
  * @param {Server} http_server
391
398
  * @param {{ path?: string, heartbeat_ms?: number, refresh_debounce_ms?: number }} [options]
392
399
  * @returns {{ wss: WebSocketServer, broadcast: (type: MessageType, payload?: unknown) => void, scheduleListRefresh: () => void }}
@@ -451,6 +458,7 @@ export function attachWsServer(http_server, options = {}) {
451
458
 
452
459
  /**
453
460
  * Broadcast a server-initiated event to all open clients.
461
+ *
454
462
  * @param {MessageType} type
455
463
  * @param {unknown} [payload]
456
464
  */
@@ -478,6 +486,7 @@ export function attachWsServer(http_server, options = {}) {
478
486
 
479
487
  /**
480
488
  * Handle an incoming message frame and respond to the same socket.
489
+ *
481
490
  * @param {WebSocket} ws
482
491
  * @param {RawData} data
483
492
  */