beads-ui 0.1.2 → 0.3.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 +29 -2
- package/README.md +39 -45
- package/app/data/list-selectors.js +98 -0
- package/app/data/providers.js +25 -127
- package/app/data/sort.js +45 -0
- package/app/data/subscription-issue-store.js +161 -0
- package/app/data/subscription-issue-stores.js +102 -0
- package/app/data/subscriptions-store.js +219 -0
- package/app/index.html +8 -0
- package/app/main.js +483 -61
- package/app/protocol.js +10 -14
- package/app/protocol.md +21 -19
- package/app/router.js +45 -9
- package/app/state.js +27 -11
- package/app/styles.css +373 -184
- package/app/utils/issue-id-renderer.js +71 -0
- package/app/utils/issue-url.js +9 -0
- package/app/utils/markdown.js +15 -194
- package/app/utils/priority-badge.js +0 -2
- package/app/utils/status-badge.js +0 -1
- package/app/utils/toast.js +34 -0
- package/app/utils/type-badge.js +0 -3
- package/app/views/board.js +439 -87
- package/app/views/detail.js +364 -154
- package/app/views/epics.js +128 -76
- package/app/views/issue-dialog.js +163 -0
- package/app/views/issue-row.js +10 -11
- package/app/views/list.js +164 -93
- package/app/views/new-issue-dialog.js +345 -0
- package/app/ws.js +36 -9
- package/bin/bdui.js +1 -1
- package/docs/adr/001-push-only-lists.md +134 -0
- package/docs/adr/002-per-subscription-stores-and-full-issue-push.md +200 -0
- package/docs/architecture.md +35 -85
- package/docs/data-exchange-subscription-plan.md +198 -0
- package/docs/db-watching.md +2 -1
- package/docs/migration-v2.md +54 -0
- package/docs/protocol/issues-push-v2.md +179 -0
- package/docs/subscription-issue-store.md +112 -0
- package/package.json +11 -3
- package/server/bd.js +0 -2
- package/server/cli/commands.js +12 -5
- package/server/cli/daemon.js +12 -5
- package/server/cli/index.js +34 -5
- package/server/cli/usage.js +2 -2
- package/server/config.js +12 -6
- package/server/db.js +0 -1
- package/server/index.js +9 -5
- package/server/list-adapters.js +218 -0
- package/server/subscriptions.js +277 -0
- package/server/validators.js +111 -0
- package/server/watcher.js +6 -9
- package/server/ws.js +466 -227
- package/docs/quickstart.md +0 -142
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# ADR 002 — Per‑Subscription Stores and Full‑Issue Push (Breaking)
|
|
2
|
+
|
|
3
|
+
```
|
|
4
|
+
Date: 2025-10-26
|
|
5
|
+
Status: Proposed (ready for owner approval)
|
|
6
|
+
Owner: agent
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Context
|
|
10
|
+
|
|
11
|
+
The UI currently maintains a central `issues` cache and a separate list
|
|
12
|
+
membership model. Push events update the central cache and lists fan out from
|
|
13
|
+
it. This split increases cognitive load (two caches, two sets of selectors),
|
|
14
|
+
creates subtle ordering/dedup bugs, and complicates tests and routing.
|
|
15
|
+
|
|
16
|
+
We want a simpler, local model per visible list: one subscription → one store →
|
|
17
|
+
one push update stream → one rendered list. Push events must contain complete
|
|
18
|
+
issue objects for correctness and to avoid fan‑out to a central cache.
|
|
19
|
+
|
|
20
|
+
## Decision
|
|
21
|
+
|
|
22
|
+
- Adopt a per‑subscription issue store (`SubscriptionIssueStore`) keyed by the
|
|
23
|
+
client’s subscription id.
|
|
24
|
+
- Server sends per‑subscription full‑issue payloads only; no id‑only deltas.
|
|
25
|
+
Messages are serialized per subscription and revisioned.
|
|
26
|
+
- Lists render exclusively from their own store snapshots; the central issue
|
|
27
|
+
cache is removed from the list render path.
|
|
28
|
+
- Breaking change: remove legacy id‑only list deltas and any compatibility
|
|
29
|
+
paths/flags. No phased rollout and no telemetry collection.
|
|
30
|
+
|
|
31
|
+
## Protocol (Server → Client)
|
|
32
|
+
|
|
33
|
+
Message shapes are defined in `types/subscriptions.ts` and documented in
|
|
34
|
+
`docs/data-exchange-subscription-plan.md`.
|
|
35
|
+
|
|
36
|
+
All envelopes include a version tag and a per‑subscription, strictly monotonic
|
|
37
|
+
`revision` used for ordering and replay protection (see UI‑144).
|
|
38
|
+
|
|
39
|
+
- `subscribed` `{ id: string }`
|
|
40
|
+
- `snapshot` `{ id: string, revision: number, issues: Issue[] }`
|
|
41
|
+
- `upsert` `{ id: string, revision: number, issue: Issue }`
|
|
42
|
+
- `delete` `{ id: string, revision: number, issue_id: string }`
|
|
43
|
+
- `error` `{ id?: string, code: string, message: string, details?: object }`
|
|
44
|
+
|
|
45
|
+
Notes
|
|
46
|
+
|
|
47
|
+
- Per‑subscription ordering is guaranteed by the server and signaled via
|
|
48
|
+
`revision`. Clients MUST apply envelopes in `revision` order and ignore any
|
|
49
|
+
envelope whose `revision` is ≤ the last applied.
|
|
50
|
+
- Clients MUST treat updates as idempotent and MAY additionally guard on an
|
|
51
|
+
`issue.updated_at` timestamp to ignore stale `upsert`s that race with local
|
|
52
|
+
state. Timestamps are advisory; `revision` is canonical for ordering.
|
|
53
|
+
- Initial state arrives as a `snapshot` with a complete list of issues for the
|
|
54
|
+
subscription key.
|
|
55
|
+
|
|
56
|
+
## Client Store API
|
|
57
|
+
|
|
58
|
+
The UI manages one store per active subscription. Minimal API surface:
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
// types only — see types/subscription-issue-store.ts
|
|
62
|
+
export interface SubscriptionIssueStore {
|
|
63
|
+
/** Client subscription id this store belongs to. */
|
|
64
|
+
readonly id: string;
|
|
65
|
+
|
|
66
|
+
/** Attach a listener that is called after each applied message. */
|
|
67
|
+
subscribe(listener: () => void): () => void;
|
|
68
|
+
|
|
69
|
+
/** Apply a push message: snapshot, upsert, or delete. */
|
|
70
|
+
applyPush(msg: SnapshotMsg | UpsertMsg | DeleteMsg): void;
|
|
71
|
+
|
|
72
|
+
/** Read-only, stable snapshot for rendering (deterministic sort). */
|
|
73
|
+
snapshot(): readonly Issue[];
|
|
74
|
+
|
|
75
|
+
/** Lookup helpers used by views/tests. */
|
|
76
|
+
size(): number;
|
|
77
|
+
getById(id: string): Issue | undefined;
|
|
78
|
+
|
|
79
|
+
/** Release references and listeners when the view unmounts. */
|
|
80
|
+
dispose(): void;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export type SnapshotMsg = {
|
|
84
|
+
type: 'snapshot';
|
|
85
|
+
id: string;
|
|
86
|
+
revision: number;
|
|
87
|
+
issues: Issue[];
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export type UpsertMsg = {
|
|
91
|
+
type: 'upsert';
|
|
92
|
+
id: string;
|
|
93
|
+
revision: number;
|
|
94
|
+
issue: Issue;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export type DeleteMsg = {
|
|
98
|
+
type: 'delete';
|
|
99
|
+
id: string;
|
|
100
|
+
revision: number;
|
|
101
|
+
issue_id: string;
|
|
102
|
+
};
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Sorting and identity
|
|
106
|
+
|
|
107
|
+
- Stores maintain stable item identity across updates (same object ref for the
|
|
108
|
+
same `id` when only fields change) and expose a deterministic sort order
|
|
109
|
+
suitable for the owning view (e.g., Issues: priority asc, then `created_at`
|
|
110
|
+
desc, then id asc).
|
|
111
|
+
|
|
112
|
+
### Error handling and reconnect
|
|
113
|
+
|
|
114
|
+
- On disconnect/reconnect, the client creates a fresh store and re‑subscribes.
|
|
115
|
+
The server sends a fresh snapshot; no attempt is made to diff across sessions.
|
|
116
|
+
|
|
117
|
+
### Reconcile algorithm (pseudo‑code)
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
state: Map<string, Issue> = new Map()
|
|
121
|
+
lastRevision: number = 0
|
|
122
|
+
|
|
123
|
+
function applyPush(msg) {
|
|
124
|
+
if (msg.revision <= lastRevision) return // stale or duplicate
|
|
125
|
+
lastRevision = msg.revision
|
|
126
|
+
|
|
127
|
+
switch (msg.type) {
|
|
128
|
+
case 'snapshot':
|
|
129
|
+
state.clear()
|
|
130
|
+
for (const it of deterministicallySort(msg.issues)) {
|
|
131
|
+
state.set(it.id, it)
|
|
132
|
+
}
|
|
133
|
+
break
|
|
134
|
+
case 'upsert':
|
|
135
|
+
const existing = state.get(msg.issue.id)
|
|
136
|
+
if (!existing || existing.updated_at <= msg.issue.updated_at) {
|
|
137
|
+
state.set(msg.issue.id, msg.issue)
|
|
138
|
+
}
|
|
139
|
+
break
|
|
140
|
+
case 'delete':
|
|
141
|
+
state.delete(msg.issue_id)
|
|
142
|
+
break
|
|
143
|
+
}
|
|
144
|
+
notifyListeners()
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
The sort function must be deterministic and view‑specific (e.g., priority asc,
|
|
149
|
+
then `created_at` desc, then `id` asc). Stores keep object identity stable for
|
|
150
|
+
the same `id` whenever fields change.
|
|
151
|
+
|
|
152
|
+
## Migration
|
|
153
|
+
|
|
154
|
+
- Delete list render paths that read via the central issues cache.
|
|
155
|
+
- Introduce a factory `createSubscriptionIssueStore(id)` at view mount; wire the
|
|
156
|
+
push client to route `snapshot`/`upsert`/`delete` messages by `id` to the
|
|
157
|
+
corresponding store via `applyPush`.
|
|
158
|
+
- Update list components to render from `store.snapshot()` and subscribe to
|
|
159
|
+
re‑render on changes.
|
|
160
|
+
- Remove legacy central‑store fan‑out and dead selectors.
|
|
161
|
+
|
|
162
|
+
## Consequences
|
|
163
|
+
|
|
164
|
+
Pros
|
|
165
|
+
|
|
166
|
+
- One‑to‑one mapping of subscription → store → view simplifies reasoning and
|
|
167
|
+
testing.
|
|
168
|
+
- No cache fan‑out; updates apply once per subscription and render once.
|
|
169
|
+
- Clearer ownership boundaries; easier disposal on route/tab changes.
|
|
170
|
+
|
|
171
|
+
Cons / Risks
|
|
172
|
+
|
|
173
|
+
- Larger `updated` payloads vs id‑only membership deltas. Mitigated by
|
|
174
|
+
per‑subscription scoping and batching.
|
|
175
|
+
- Requires coordinated server/client cutover due to the breaking change.
|
|
176
|
+
|
|
177
|
+
## Alternatives Considered
|
|
178
|
+
|
|
179
|
+
- Keep the central cache and fan‑out membership to lists. Rejected: duplicates
|
|
180
|
+
ownership, increases complexity and test surface, caused known ordering bugs.
|
|
181
|
+
- Maintain id‑only list deltas with separate issue fetches. Rejected: adds
|
|
182
|
+
round‑trips and cross‑store coordination; does not meet simplicity goal.
|
|
183
|
+
- Dual protocol/feature flag with gradual cutover. Rejected per epic scope: no
|
|
184
|
+
flags, no compatibility layer, and no telemetry collection.
|
|
185
|
+
|
|
186
|
+
## Related
|
|
187
|
+
|
|
188
|
+
- ADR 001 — Push‑Only Lists (v2): establishes push‑only direction and server
|
|
189
|
+
batching; this ADR replaces the central‑store + list‑membership split with a
|
|
190
|
+
per‑subscription store model.
|
|
191
|
+
- `docs/protocol/issues-push-v2.md` and
|
|
192
|
+
`docs/data-exchange-subscription-plan.md` for normative protocol and server
|
|
193
|
+
behavior.
|
|
194
|
+
|
|
195
|
+
## Status & Follow‑ups
|
|
196
|
+
|
|
197
|
+
- This ADR is a breaking change with no flags/compat/telemetry. It becomes
|
|
198
|
+
Accepted once UI‑166 is approved by frontend and backend owners and the
|
|
199
|
+
server/client work (UI‑167, UI‑168, UI‑169) lands.
|
|
200
|
+
- Cleanup (UI-174) removes the central store and delta fan‑out code.
|
package/docs/architecture.md
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
|
-
# beads-ui Architecture
|
|
1
|
+
# beads-ui Architecture (v2)
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
Note (2025-10-26)
|
|
4
|
+
|
|
5
|
+
- beads-ui has migrated to a push‑only protocol for lists and details. The
|
|
6
|
+
server no longer implements legacy read RPCs `list-issues` and `epic-status`.
|
|
7
|
+
For the normative protocol reference, see `docs/protocol/issues-push-v2.md`.
|
|
8
|
+
|
|
9
|
+
This document describes the high‑level architecture of beads‑ui and the v2
|
|
10
|
+
push‑only data flow used between the browser SPA and the local Node.js server.
|
|
5
11
|
|
|
6
12
|
## Overview
|
|
7
13
|
|
|
@@ -45,7 +51,7 @@ WebSocket protocol used between the browser SPA and the local Node.js server.
|
|
|
45
51
|
- bd bridge: `server/bd.js` (spawn `bd`, inject `--db` consistently, JSON
|
|
46
52
|
helpers)
|
|
47
53
|
- DB resolution/watch: `server/db.js` (resolve active DB path),
|
|
48
|
-
`server/watcher.js` (
|
|
54
|
+
`server/watcher.js` (schedule list refresh)
|
|
49
55
|
- Config: `server/config.js` (bind to `127.0.0.1`, default port 3000)
|
|
50
56
|
|
|
51
57
|
## Data Flow
|
|
@@ -55,105 +61,52 @@ WebSocket protocol used between the browser SPA and the local Node.js server.
|
|
|
55
61
|
2. Server validates and maps the request to a `bd` command (no shell; args array
|
|
56
62
|
only).
|
|
57
63
|
3. Server replies with `{ id, ok, type, payload }` or `{ id, ok:false, error }`.
|
|
58
|
-
4. Independent of requests, the DB watcher
|
|
59
|
-
clients.
|
|
64
|
+
4. Independent of requests, the DB watcher schedules a refresh for active list
|
|
65
|
+
subscriptions; clients receive `snapshot`/`upsert`/`delete` envelopes.
|
|
60
66
|
|
|
61
|
-
## Protocol (
|
|
67
|
+
## Protocol (v2.0.0)
|
|
62
68
|
|
|
63
|
-
Envelope shapes (see `app/protocol.
|
|
69
|
+
Envelope shapes (see `app/protocol.md` and `docs/protocol/issues-push-v2.md`):
|
|
64
70
|
|
|
65
|
-
- Request: `{ id: string, type:
|
|
71
|
+
- Request: `{ id: string, type: string, payload?: any }`
|
|
66
72
|
- Reply:
|
|
67
|
-
`{ id: string, ok: boolean, type:
|
|
73
|
+
`{ id: string, ok: boolean, type: string, payload?: any, error?: { code: string, message: string, details?: any } }`
|
|
74
|
+
|
|
75
|
+
Push‑only subscriptions for lists and details:
|
|
76
|
+
|
|
77
|
+
- Client subscribes via `subscribe-list` with `{ id, type, params? }`.
|
|
78
|
+
- Server acks the subscription and immediately publishes a `snapshot` envelope
|
|
79
|
+
with the full list for that subscription id followed by `upsert`/`delete`
|
|
80
|
+
envelopes as data changes. Clients render from local per‑subscription stores.
|
|
68
81
|
|
|
69
|
-
|
|
82
|
+
Common message types (mutations only; list reads removed):
|
|
70
83
|
|
|
71
|
-
- `list-issues` payload: `{ filters?: { status?: string, priority?: number } }`
|
|
72
|
-
- `show-issue` payload: `{ id: string }`
|
|
73
84
|
- `update-status` payload:
|
|
74
85
|
`{ id: string, status: 'open'|'in_progress'|'closed' }`
|
|
75
86
|
- `edit-text` payload:
|
|
76
|
-
`{ id: string, field: 'title'|'description'|'acceptance', value: string }`
|
|
87
|
+
`{ id: string, field: 'title'|'description'|'acceptance'|'notes'|'design', value: string }`
|
|
77
88
|
- `update-priority` payload: `{ id: string, priority: 0|1|2|3|4 }`
|
|
78
89
|
- `dep-add` payload: `{ a: string, b: string, view_id?: string }`
|
|
79
90
|
- `dep-remove` payload: `{ a: string, b: string, view_id?: string }`
|
|
80
|
-
- `
|
|
81
|
-
`{
|
|
82
|
-
|
|
83
|
-
Defined in the spec but not yet handled on the server:
|
|
84
|
-
|
|
85
|
-
- `create-issue`, `list-ready`, `subscribe-updates` (client sends on connect;
|
|
86
|
-
ignored safely)
|
|
87
|
-
|
|
88
|
-
### Examples
|
|
91
|
+
- `create-issue` payload:
|
|
92
|
+
`{ title: string, type?: 'bug'|'feature'|'task'|'epic'|'chore', priority?: 0|1|2|3|4, description?: string }`
|
|
89
93
|
|
|
90
|
-
|
|
94
|
+
Removed in v2:
|
|
91
95
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
"id": "r1",
|
|
95
|
-
"type": "list-issues",
|
|
96
|
-
"payload": { "filters": { "status": "open" } }
|
|
97
|
-
}
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
Reply
|
|
101
|
-
|
|
102
|
-
```json
|
|
103
|
-
{
|
|
104
|
-
"id": "r1",
|
|
105
|
-
"ok": true,
|
|
106
|
-
"type": "list-issues",
|
|
107
|
-
"payload": [{ "id": "UI-1", "title": "..." }]
|
|
108
|
-
}
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
Update status
|
|
112
|
-
|
|
113
|
-
```json
|
|
114
|
-
{
|
|
115
|
-
"id": "r2",
|
|
116
|
-
"type": "update-status",
|
|
117
|
-
"payload": { "id": "UI-1", "status": "in_progress" }
|
|
118
|
-
}
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
Server push (watcher)
|
|
122
|
-
|
|
123
|
-
```json
|
|
124
|
-
{
|
|
125
|
-
"id": "evt-1732212345000",
|
|
126
|
-
"ok": true,
|
|
127
|
-
"type": "issues-changed",
|
|
128
|
-
"payload": { "ts": 1732212345000 }
|
|
129
|
-
}
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
Error reply
|
|
133
|
-
|
|
134
|
-
```json
|
|
135
|
-
{
|
|
136
|
-
"id": "r3",
|
|
137
|
-
"ok": false,
|
|
138
|
-
"type": "show-issue",
|
|
139
|
-
"error": { "code": "not_found", "message": "Issue UI-99" }
|
|
140
|
-
}
|
|
141
|
-
```
|
|
96
|
+
- `list-issues`, `epic-status`, `subscribe-updates` and the legacy
|
|
97
|
+
`issues-changed` event.
|
|
142
98
|
|
|
143
99
|
## UI → bd Command Mapping
|
|
144
100
|
|
|
145
|
-
-
|
|
146
|
-
- Show: `bd show <id> --json`
|
|
101
|
+
- Lists and details: push‑only via `subscribe-list` (no list reads)
|
|
147
102
|
- Update status: `bd update <id> --status <open|in_progress|closed>`
|
|
148
103
|
- Update priority: `bd update <id> --priority <0..4>`
|
|
149
104
|
- Edit title: `bd update <id> --title <text>`
|
|
150
|
-
- Edit description:
|
|
105
|
+
- Edit description: `bd update <id> --description <text>`
|
|
151
106
|
- Edit acceptance: `bd update <id> --acceptance-criteria <text>`
|
|
152
107
|
- Link dependency: `bd dep add <a> <b>` (a depends on b)
|
|
153
108
|
- Unlink dependency: `bd dep remove <a> <b>`
|
|
154
|
-
-
|
|
155
|
-
`bd create "title" -t <type> -p <prio> -d "desc"`; Ready list:
|
|
156
|
-
`bd ready --json`
|
|
109
|
+
- Create issue: `bd create "title" -t <type> -p <prio> -d "desc"`
|
|
157
110
|
|
|
158
111
|
Rationale
|
|
159
112
|
|
|
@@ -199,9 +152,6 @@ Notes
|
|
|
199
152
|
- Error object: `{ code: string, message: string, details?: any }`
|
|
200
153
|
- Common codes: `bad_request`, `not_found`, `bd_error`, `unknown_type`,
|
|
201
154
|
`bad_json`
|
|
202
|
-
- Versioning: `PROTOCOL_VERSION` in `app/protocol.js` (currently `1.0.0`).
|
|
203
|
-
Breaking changes increment this value; additive message types are backwards
|
|
204
|
-
compatible.
|
|
205
155
|
|
|
206
156
|
## Security and Local Boundaries
|
|
207
157
|
|
|
@@ -214,8 +164,8 @@ Notes
|
|
|
214
164
|
|
|
215
165
|
- The server resolves the active beads SQLite DB path (see
|
|
216
166
|
`docs/db-watching.md`).
|
|
217
|
-
- File watcher
|
|
218
|
-
|
|
167
|
+
- File watcher schedules list refresh; the server publishes subscription
|
|
168
|
+
envelopes. UI re-renders from local per-subscription stores.
|
|
219
169
|
|
|
220
170
|
## Risks & Open Questions
|
|
221
171
|
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# Data Exchange Model — Subscription‑Based Updates (Full‑Issue)
|
|
2
|
+
|
|
3
|
+
```
|
|
4
|
+
Date: 2025-10-25
|
|
5
|
+
Status: Implemented
|
|
6
|
+
Owner: agent
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Goals
|
|
10
|
+
|
|
11
|
+
- Replace ad-hoc list fetching with subscription-based incremental updates.
|
|
12
|
+
- Minimize complexity; send full‑issue payloads in envelopes targeted to a
|
|
13
|
+
specific subscription key.
|
|
14
|
+
- Ensure consistent, race-free updates around user-triggered mutations.
|
|
15
|
+
- Keep UI models per-subscription to simplify rendering and memory usage.
|
|
16
|
+
|
|
17
|
+
## Scope
|
|
18
|
+
|
|
19
|
+
- Server and client for `beads-ui`.
|
|
20
|
+
- Uses `bd` CLI for data access; no DB schema changes.
|
|
21
|
+
|
|
22
|
+
## Subscription Types
|
|
23
|
+
|
|
24
|
+
- `all-issues`
|
|
25
|
+
- `epics` // Removed: `issues-for-epic` (use `issue-detail` for the epic and
|
|
26
|
+
render its `dependents`)
|
|
27
|
+
- `blocked-issues`
|
|
28
|
+
- `ready-issues`
|
|
29
|
+
- `in-progress-issues`
|
|
30
|
+
- `closed-issues` (special filtering noted below)
|
|
31
|
+
|
|
32
|
+
## Server Architecture
|
|
33
|
+
|
|
34
|
+
### Subscription Registry (Issue List Subscriptions)
|
|
35
|
+
|
|
36
|
+
- Keyed by `subscriptionKey = type + JSON.stringify(params)`.
|
|
37
|
+
- Value:
|
|
38
|
+
`{ itemsById: Map<string, { updated_at: string, closed_at: string|null }>, subscribers: Set<SubscriberId>, lastRunAt?: number }`.
|
|
39
|
+
- Each subscribe request either attaches to an existing registry entry or
|
|
40
|
+
creates a new one.
|
|
41
|
+
- No TTL: subscriptions are evicted only on WebSocket disconnect. Unsubscribe
|
|
42
|
+
removes a subscriber from the set but keeps the registry entry until the
|
|
43
|
+
connection closes.
|
|
44
|
+
|
|
45
|
+
### Mapping to `bd` Commands
|
|
46
|
+
|
|
47
|
+
- `all-issues` → `bd list` (default/open)
|
|
48
|
+
- `epics` → `bd list --type epic` (or equivalent)
|
|
49
|
+
- `detail:{id}` → `bd show <id> --json` (use `dependents` from the epic detail
|
|
50
|
+
for children)
|
|
51
|
+
- `blocked-issues` → `bd list --blocked`
|
|
52
|
+
- `ready-issues` → `bd ready --limit 1000`
|
|
53
|
+
- `in-progress-issues` → `bd list --status in_progress`
|
|
54
|
+
- `closed-issues` → `bd list --status closed` (then filter first; see Special
|
|
55
|
+
Cases)
|
|
56
|
+
|
|
57
|
+
Notes:
|
|
58
|
+
|
|
59
|
+
- Exact flags depend on `bd`; create adapters that encapsulate CLI details and
|
|
60
|
+
normalize results.
|
|
61
|
+
|
|
62
|
+
### Refresh Algorithm (per run)
|
|
63
|
+
|
|
64
|
+
1. Execute mapped `bd` command to get the full list of `issues` for the spec.
|
|
65
|
+
2. If subscription is `closed-issues` with a filter, apply it before step 3.
|
|
66
|
+
3. Compare with the registry’s last known items for this subscription key.
|
|
67
|
+
4. For new or changed items, emit `upsert` envelopes with the full issue payload
|
|
68
|
+
to all subscribers of the key on the current connection.
|
|
69
|
+
5. For removed items, emit `delete` envelopes with `issue_id`.
|
|
70
|
+
6. Update the registry’s state for the key.
|
|
71
|
+
|
|
72
|
+
### Special Case: Closed Issues Filtering
|
|
73
|
+
|
|
74
|
+
- Apply `since` filter (epoch milliseconds) before diffing to avoid spurious
|
|
75
|
+
updates when reloading older closed items. Only items with
|
|
76
|
+
`closed_at >= since` are included. Invalid or non-positive `since` values are
|
|
77
|
+
ignored.
|
|
78
|
+
- Filters are part of subscription params to keep deterministic diffing.
|
|
79
|
+
|
|
80
|
+
### Migration
|
|
81
|
+
|
|
82
|
+
This change replaces request/response list reads and id‑only deltas with
|
|
83
|
+
subscription‑based, full‑issue push envelopes.
|
|
84
|
+
|
|
85
|
+
Client migration steps:
|
|
86
|
+
|
|
87
|
+
- Replace list fetch calls with `subscribe-list`/`unsubscribe-list` messages.
|
|
88
|
+
- Maintain a per‑subscription local store keyed by the client `id`.
|
|
89
|
+
- Apply `snapshot`/`upsert`/`delete` envelopes in revision order; render from
|
|
90
|
+
`store.snapshot()`.
|
|
91
|
+
- Remove any legacy polling timers; updates now arrive via server push.
|
|
92
|
+
- For closed issue feeds, pass a `params.since` value (epoch ms) that reflects
|
|
93
|
+
the UI’s filter horizon if needed server‑side.
|
|
94
|
+
|
|
95
|
+
### Watcher Integration (DB Updates)
|
|
96
|
+
|
|
97
|
+
- A file/DB watcher signals any data change.
|
|
98
|
+
- On signal, for each active subscription: re-run its mapped `bd` command → diff
|
|
99
|
+
→ push deltas to all subscribers.
|
|
100
|
+
- Backpressure: coalesce multiple watcher events into a single run per
|
|
101
|
+
subscription (leading-edge, with trailing-edge within 50–100ms).
|
|
102
|
+
|
|
103
|
+
### User Mutations (Race Control)
|
|
104
|
+
|
|
105
|
+
When client requests a change (e.g., update status):
|
|
106
|
+
|
|
107
|
+
1. Execute the explicit protocol mutation (mapped to a concrete `bd` command
|
|
108
|
+
under the hood; no arbitrary commands allowed).
|
|
109
|
+
2. In parallel, attach a once-listener to the watcher that resolves on the next
|
|
110
|
+
change event (no debounce) or a 500ms timeout, whichever occurs first.
|
|
111
|
+
3. After the promise resolves, for each affected subscription, run the standard
|
|
112
|
+
refresh/diff/push routine exactly once.
|
|
113
|
+
4. During the pending mutation window, suppress watcher-triggered refreshes for
|
|
114
|
+
affected subscriptions to avoid duplicate pushes.
|
|
115
|
+
|
|
116
|
+
### Error Handling
|
|
117
|
+
|
|
118
|
+
- Validate subscription params; return structured errors.
|
|
119
|
+
- For `bd` failures, include stderr and exit code; do not crash subscriptions.
|
|
120
|
+
- If a subscriber disconnects mid-push, drop silently and clean up.
|
|
121
|
+
|
|
122
|
+
## Client Architecture
|
|
123
|
+
|
|
124
|
+
### Local Store per Subscription
|
|
125
|
+
|
|
126
|
+
- Keyed by `subscriptionKey`.
|
|
127
|
+
- Value: `{ itemsById: Map<string, Issue>, lastAppliedAt: number }`.
|
|
128
|
+
- On `{ added, updated, removed }`, update `itemsById` accordingly and request
|
|
129
|
+
view re-render.
|
|
130
|
+
- Tabs and epic expansion toggle subscribe/unsubscribe appropriately.
|
|
131
|
+
|
|
132
|
+
### UI Flow
|
|
133
|
+
|
|
134
|
+
- Tab switch: unsubscribe previous, subscribe new.
|
|
135
|
+
- Epic toggle: subscribe/unsubscribe `detail:{id}` with
|
|
136
|
+
`{ type: 'issue-detail', params: { id } }`.
|
|
137
|
+
- Components derive view state from the local store snapshot.
|
|
138
|
+
|
|
139
|
+
## Wire Protocol (vNext)
|
|
140
|
+
|
|
141
|
+
### Messages: Client → Server
|
|
142
|
+
|
|
143
|
+
- `subscribe-list` `{ id: string, type: string, params?: object }`
|
|
144
|
+
- `unsubscribe-list` `{ id: string }`
|
|
145
|
+
- Explicit mutation messages (enumerated in the protocol; no generic command
|
|
146
|
+
pipe). The set mirrors the main protocol (update-status, edit-text,
|
|
147
|
+
update-priority, update-assignee, create-issue, dep-add/remove,
|
|
148
|
+
label-add/remove).
|
|
149
|
+
|
|
150
|
+
### Messages: Server → Client (Per‑Subscription)
|
|
151
|
+
|
|
152
|
+
All envelopes include a per‑subscription `revision` (monotonic, starting at 1),
|
|
153
|
+
and the client subscription `id`.
|
|
154
|
+
|
|
155
|
+
- `snapshot` `{ id, schema, revision, issues: Issue[] }`
|
|
156
|
+
- `upsert` `{ id, schema, revision, issue: Issue }`
|
|
157
|
+
- `delete` `{ id, schema, revision, issue_id: string }`
|
|
158
|
+
|
|
159
|
+
Notes
|
|
160
|
+
|
|
161
|
+
- Initial subscribe triggers a single `snapshot` for the requesting `id` only.
|
|
162
|
+
- Subsequent refresh runs emit `upsert`/`delete` events to all subscribers of
|
|
163
|
+
the same subscription key on that connection.
|
|
164
|
+
- Clients MUST apply envelopes in `revision` order and ignore stale revisions.
|
|
165
|
+
|
|
166
|
+
## Concurrency & Ordering Guarantees
|
|
167
|
+
|
|
168
|
+
- Per-subscription ordering: server serializes diff runs per key.
|
|
169
|
+
- Deltas are applied in order on the client; no interleaving for a given `id`.
|
|
170
|
+
- Mutations provide “eventually up-to-date” guarantee via the once-listener +
|
|
171
|
+
timeout.
|
|
172
|
+
|
|
173
|
+
## Observability
|
|
174
|
+
|
|
175
|
+
- Basic development logging only; no telemetry collection for message rates.
|
|
176
|
+
|
|
177
|
+
## Security
|
|
178
|
+
|
|
179
|
+
- Only explicit mutation operations are implemented by the protocol; no
|
|
180
|
+
arbitrary commands from clients.
|
|
181
|
+
- Reject unknown subscription types; enforce param schemas.
|
|
182
|
+
|
|
183
|
+
## Testing Strategy
|
|
184
|
+
|
|
185
|
+
- Unit: diffing, registry, adapter mapping, filter logic.
|
|
186
|
+
- Integration: watcher → refresh → push flow; mutation window once-only
|
|
187
|
+
behavior.
|
|
188
|
+
- E2E: tab switching, epic expansion, status changes while updates stream.
|
|
189
|
+
|
|
190
|
+
## Release Notes
|
|
191
|
+
|
|
192
|
+
- Breaking change: Clients must adopt `snapshot`/`upsert`/`delete` envelopes and
|
|
193
|
+
per‑subscription stores. Previous polling and id‑only list deltas are removed.
|
|
194
|
+
|
|
195
|
+
## Open Questions
|
|
196
|
+
|
|
197
|
+
- Exact `bd` flags for each list type; confirm and codify.
|
|
198
|
+
- Closed-issue filter semantics (date range vs. other criteria).
|
package/docs/db-watching.md
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# DB Watching and Resolution
|
|
2
2
|
|
|
3
3
|
The server watches the active beads SQLite database file for changes and
|
|
4
|
-
|
|
4
|
+
schedules a refresh of active list subscriptions. Clients receive
|
|
5
|
+
`snapshot`/`upsert`/`delete` envelopes for their active subscriptions.
|
|
5
6
|
|
|
6
7
|
## Resolution Order
|
|
7
8
|
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Migration: Push‑Only Per‑Subscription Stores (Breaking)
|
|
2
|
+
|
|
3
|
+
```
|
|
4
|
+
Date: 2025-10-26
|
|
5
|
+
Status: Final
|
|
6
|
+
Owner: agent
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
This release replaces legacy list reads and id‑only list deltas with
|
|
10
|
+
per‑subscription push envelopes that carry full issue payloads. There is no
|
|
11
|
+
compatibility mode and no feature flags.
|
|
12
|
+
|
|
13
|
+
## Required Versions
|
|
14
|
+
|
|
15
|
+
- beads‑ui: 0.2.0 or later (includes the server)
|
|
16
|
+
- Node.js: >= 22 (see `package.json` engines)
|
|
17
|
+
|
|
18
|
+
Upgrade:
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
npm i -g beads-ui@latest
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## What Changed
|
|
25
|
+
|
|
26
|
+
- New protocol: `snapshot` / `upsert` / `delete` envelopes and a
|
|
27
|
+
per‑subscription `revision`.
|
|
28
|
+
- One store per list: views render from a `SubscriptionIssueStore` created for
|
|
29
|
+
each active subscription id.
|
|
30
|
+
- Removed: central issues store and delta fan‑out.
|
|
31
|
+
- Removed: legacy read RPCs `list-issues` and `epic-status`.
|
|
32
|
+
|
|
33
|
+
## Migration Checklist
|
|
34
|
+
|
|
35
|
+
- Replace list reads with `subscribe-list`/`unsubscribe-list`.
|
|
36
|
+
- Create a `SubscriptionIssueStore` at view mount and wire the WS client to call
|
|
37
|
+
`store.applyPush(payload)` for `snapshot`/`upsert`/`delete`.
|
|
38
|
+
- Render from `store.snapshot()`; remove code paths that read from a central
|
|
39
|
+
issue cache.
|
|
40
|
+
- Delete dead selectors/helpers that depended on the central cache.
|
|
41
|
+
- Verify reconnect flows: a fresh `snapshot` (rev 1) replaces state cleanly.
|
|
42
|
+
|
|
43
|
+
## Notes
|
|
44
|
+
|
|
45
|
+
- No telemetry or phased rollout was implemented; ensure the UI and server are
|
|
46
|
+
updated together. Older clients will not function with the new server.
|
|
47
|
+
- For closed‑issues feeds, prefer passing a `since` param where applicable to
|
|
48
|
+
keep snapshots small.
|
|
49
|
+
|
|
50
|
+
## References
|
|
51
|
+
|
|
52
|
+
- `docs/protocol/issues-push-v2.md`
|
|
53
|
+
- `docs/subscription-issue-store.md`
|
|
54
|
+
- ADR 002 — Per‑Subscription Stores and Full‑Issue Push
|