dsh-session-mng 0.1.4 → 0.2.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/lib/api.js CHANGED
@@ -18,6 +18,9 @@ export const API_PREFIX = '/session-mng/api'
18
18
  /** Request body ceiling; these requests are tiny id lists or one title. */
19
19
  const MAX_BODY_BYTES = 64 * 1024
20
20
 
21
+ /** Budget for one read request: a catalog read plus, at worst, one size scan. */
22
+ const GET_TIMEOUT_MS = 20_000
23
+
21
24
  /** Write one JSON response. */
22
25
  function sendJson(res, status, body) {
23
26
  if (res.writableEnded) return
@@ -59,7 +62,16 @@ async function readJsonBody(req) {
59
62
  }
60
63
  }
61
64
 
62
- /** One dispatch table entry. `needsBody` is false only for the state read. */
65
+ /** Read one GET query string into a plain parameter object. */
66
+ function queryOf(url) {
67
+ const params = {}
68
+ for (const [key, value] of url.searchParams) {
69
+ if (!Object.hasOwn(params, key)) params[key] = value
70
+ }
71
+ return params
72
+ }
73
+
74
+ /** One dispatch table entry. `needsBody` is false only for the read routes. */
63
75
  const ROUTES = {
64
76
  preview: { needsBody: true, run: (service, body) => service.preview(body.sessionId, body.count) },
65
77
  'title/propose': {
@@ -74,6 +86,24 @@ const ROUTES = {
74
86
  move: { needsBody: true, run: (service, body) => service.move(body.sessionIds, body.workspaceId) },
75
87
  }
76
88
 
89
+ /**
90
+ * The read surface. `/state` stays byte-compatible for 0.1.x clients; the
91
+ * 0.2.0 routes serve the same data through a paginated, catalog-backed shape.
92
+ * Each entry receives the parsed query object and one request signal.
93
+ */
94
+ const GET_ROUTES = {
95
+ state: (service, params, signal) =>
96
+ service.state({ includeSubagents: params.subagents === '1', signal }),
97
+ overview: (service, params, signal) =>
98
+ service.overview({ includeSubagents: params.subagents === '1', signal }),
99
+ sessions: (service, params, signal) => service.sessions({ ...params, signal }),
100
+ search: (service, params, signal) => service.search({ ...params, signal }),
101
+ session: (service, params, signal) => service.session({ ...params, signal }),
102
+ blanks: (service, params, signal) =>
103
+ service.blanks({ includeSubagents: params.subagents === '1', signal }),
104
+ sizes: (service, params, signal) => service.sizes({ ...params, signal }),
105
+ }
106
+
77
107
  /**
78
108
  * Build the Node request handler for the plugin's prefix route.
79
109
  *
@@ -97,25 +127,28 @@ export function createApiHandler({ ctx, config, service, trustedHosts }) {
97
127
  return
98
128
  }
99
129
 
100
- if (relative === '' || relative === 'state') {
130
+ // A prefix route also answers the bare prefix; it means the legacy `/state`.
131
+ const key = relative === '' ? 'state' : relative
132
+
133
+ const readRoute = GET_ROUTES[key]
134
+ if (readRoute !== undefined) {
101
135
  if (req.method !== 'GET') {
102
- sendJson(res, 405, { ok: false, error: { code: 'method-error', message: 'state is GET-only' } })
136
+ sendJson(res, 405, { ok: false, error: { code: 'method-error', message: `${key} is GET-only` } })
103
137
  return
104
138
  }
105
- const includeSubagents = url.searchParams.get('subagents') === '1'
106
- const value = await service.state({ includeSubagents, signal: AbortSignal.timeout(20_000) })
139
+ const value = await readRoute(service, queryOf(url), AbortSignal.timeout(GET_TIMEOUT_MS))
107
140
  sendJson(res, 200, { ok: true, value })
108
141
  return
109
142
  }
110
143
 
111
- if (req.method !== 'POST') {
112
- sendJson(res, 405, { ok: false, error: { code: 'method-error', message: 'POST only' } })
144
+ const route = ROUTES[key]
145
+ if (route === undefined) {
146
+ sendJson(res, 404, { ok: false, error: { code: 'not-found', message: `unknown route "${key}"` } })
113
147
  return
114
148
  }
115
149
 
116
- const route = ROUTES[relative]
117
- if (route === undefined) {
118
- sendJson(res, 404, { ok: false, error: { code: 'not-found', message: `unknown action "${relative}"` } })
150
+ if (req.method !== 'POST') {
151
+ sendJson(res, 405, { ok: false, error: { code: 'method-error', message: 'POST only' } })
119
152
  return
120
153
  }
121
154
 
@@ -123,7 +156,7 @@ export function createApiHandler({ ctx, config, service, trustedHosts }) {
123
156
  // A closed connection cancels a read-only model call; destructive
124
157
  // operations are deliberately allowed to finish once admitted.
125
158
  const controller = new AbortController()
126
- if (relative === 'title/propose') {
159
+ if (key === 'title/propose') {
127
160
  res.on('close', () => {
128
161
  if (!res.writableEnded) controller.abort()
129
162
  })