@prsm/devtools 1.0.2 → 1.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/README.md +14 -1
- package/dist/client/assets/index-B7_KPm6_.css +1 -0
- package/dist/client/assets/index-DWT8hGf_.js +178 -0
- package/dist/client/index.html +9 -3
- package/package.json +8 -4
- package/src/index.js +186 -8
- package/dist/client/assets/index-CkwmeR8W.js +0 -176
- package/dist/client/assets/index-p_4czaPS.css +0 -1
package/dist/client/index.html
CHANGED
|
@@ -4,11 +4,17 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>prsm devtools</title>
|
|
7
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
8
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
9
|
+
<link
|
|
10
|
+
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Space+Mono:wght@400;700&display=swap"
|
|
11
|
+
rel="stylesheet"
|
|
12
|
+
/>
|
|
7
13
|
<style>
|
|
8
|
-
body { margin: 0; background: #
|
|
14
|
+
body { margin: 0; background: #ffffff; }
|
|
9
15
|
</style>
|
|
10
|
-
<script type="module" crossorigin src="./assets/index-
|
|
11
|
-
<link rel="stylesheet" crossorigin href="./assets/index-
|
|
16
|
+
<script type="module" crossorigin src="./assets/index-DWT8hGf_.js"></script>
|
|
17
|
+
<link rel="stylesheet" crossorigin href="./assets/index-B7_KPm6_.css">
|
|
12
18
|
</head>
|
|
13
19
|
<body>
|
|
14
20
|
<div id="app"></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prsm/devtools",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Read-only Express middleware dashboard for observing @prsm infrastructure at runtime",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -31,11 +31,15 @@
|
|
|
31
31
|
"express": "^4.21.0"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
|
-
"@prsm/
|
|
35
|
-
"@prsm/
|
|
34
|
+
"@prsm/cells": "^1.2.0",
|
|
35
|
+
"@prsm/cron": "^1.1.0",
|
|
36
|
+
"@prsm/limit": "^1.2.0",
|
|
37
|
+
"@prsm/lock": "^1.1.0",
|
|
36
38
|
"@prsm/queue": "^3.0.8",
|
|
39
|
+
"@prsm/workflow": "^3.1.0",
|
|
37
40
|
"cors": "^2.8.6",
|
|
38
|
-
"dotenv": "^16.4.7"
|
|
41
|
+
"dotenv": "^16.4.7",
|
|
42
|
+
"pg": "^8.13.1"
|
|
39
43
|
},
|
|
40
44
|
"engines": {
|
|
41
45
|
"node": ">=18"
|
package/src/index.js
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
|
-
import { Router, static as serveStatic } from 'express'
|
|
1
|
+
import { Router, static as serveStatic, json } from 'express'
|
|
2
2
|
import { fileURLToPath } from 'node:url'
|
|
3
3
|
import { resolve, dirname } from 'node:path'
|
|
4
|
-
import { existsSync } from 'node:fs'
|
|
4
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
5
5
|
|
|
6
6
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
7
7
|
const clientDir = resolve(__dirname, '..', 'dist', 'client')
|
|
8
8
|
|
|
9
|
+
function normalizeCellGraphs(cells) {
|
|
10
|
+
if (!cells) return null
|
|
11
|
+
if (typeof cells.cell === 'function') return { default: cells }
|
|
12
|
+
return cells
|
|
13
|
+
}
|
|
14
|
+
|
|
9
15
|
function patternToString(p) {
|
|
10
16
|
if (typeof p === 'string') return p
|
|
11
17
|
if (p instanceof RegExp) return p.toString()
|
|
@@ -23,8 +29,10 @@ function patternToString(p) {
|
|
|
23
29
|
*/
|
|
24
30
|
export function prsmDevtools(options = {}) {
|
|
25
31
|
const router = Router()
|
|
26
|
-
const { queue, cron, limit, workflow, realtime } = options
|
|
32
|
+
const { queue, cron, limit, workflow, realtime, lock } = options
|
|
33
|
+
const cellGraphs = normalizeCellGraphs(options.cells)
|
|
27
34
|
const sseClients = new Set()
|
|
35
|
+
const jsonBody = json()
|
|
28
36
|
|
|
29
37
|
function broadcast(event, data) {
|
|
30
38
|
const msg = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`
|
|
@@ -44,6 +52,14 @@ export function prsmDevtools(options = {}) {
|
|
|
44
52
|
cron.on('error', (data) => broadcast('cron:error', { name: data.name, error: data.error?.message }))
|
|
45
53
|
}
|
|
46
54
|
|
|
55
|
+
if (cellGraphs) {
|
|
56
|
+
for (const [graphName, g] of Object.entries(cellGraphs)) {
|
|
57
|
+
g.on((name, value, state) => {
|
|
58
|
+
broadcast('cells:change', { graph: graphName, name, value, status: state.status, updatedAt: state.updatedAt })
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
47
63
|
if (workflow) {
|
|
48
64
|
for (const event of [
|
|
49
65
|
'execution:queued',
|
|
@@ -68,6 +84,8 @@ export function prsmDevtools(options = {}) {
|
|
|
68
84
|
limit: limit ? Object.keys(limit) : [],
|
|
69
85
|
workflow: !!workflow,
|
|
70
86
|
realtime: !!realtime,
|
|
87
|
+
cells: cellGraphs ? Object.keys(cellGraphs) : [],
|
|
88
|
+
lock: lock ? Object.keys(lock) : [],
|
|
71
89
|
})
|
|
72
90
|
})
|
|
73
91
|
|
|
@@ -95,6 +113,18 @@ export function prsmDevtools(options = {}) {
|
|
|
95
113
|
}))
|
|
96
114
|
res.json({ jobs })
|
|
97
115
|
})
|
|
116
|
+
|
|
117
|
+
router.post('/api/cron/:name/run', async (req, res) => {
|
|
118
|
+
if (typeof cron.run !== 'function') {
|
|
119
|
+
return res.status(400).json({ error: 'This @prsm/cron version does not support manual runs' })
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
const result = await cron.run(req.params.name)
|
|
123
|
+
res.json(result)
|
|
124
|
+
} catch (err) {
|
|
125
|
+
res.status(400).json({ error: err.message })
|
|
126
|
+
}
|
|
127
|
+
})
|
|
98
128
|
}
|
|
99
129
|
|
|
100
130
|
if (limit) {
|
|
@@ -102,13 +132,45 @@ export function prsmDevtools(options = {}) {
|
|
|
102
132
|
res.json({ limiters: Object.keys(limit) })
|
|
103
133
|
})
|
|
104
134
|
|
|
135
|
+
router.get('/api/limits/:name/keys', async (req, res) => {
|
|
136
|
+
const limiter = limit[req.params.name]
|
|
137
|
+
if (!limiter) return res.status(404).json({ error: 'Limiter not found' })
|
|
138
|
+
if (!limiter.keys) {
|
|
139
|
+
return res.status(400).json({ error: 'This @prsm/limit version does not support key listing' })
|
|
140
|
+
}
|
|
141
|
+
try {
|
|
142
|
+
const n = req.query.limit ? Number(req.query.limit) : undefined
|
|
143
|
+
const keys = await limiter.keys(n ? { limit: n } : undefined)
|
|
144
|
+
res.json({ keys })
|
|
145
|
+
} catch (err) {
|
|
146
|
+
res.status(500).json({ error: err.message })
|
|
147
|
+
}
|
|
148
|
+
})
|
|
149
|
+
|
|
105
150
|
router.get('/api/limits/:name/peek/:key', async (req, res) => {
|
|
106
151
|
const limiter = limit[req.params.name]
|
|
107
152
|
if (!limiter) return res.status(404).json({ error: 'Limiter not found' })
|
|
108
153
|
if (!limiter.peek) return res.status(400).json({ error: 'Limiter does not support peek' })
|
|
109
154
|
|
|
110
|
-
|
|
111
|
-
|
|
155
|
+
try {
|
|
156
|
+
const result = await limiter.peek(req.params.key)
|
|
157
|
+
res.json(result)
|
|
158
|
+
} catch (err) {
|
|
159
|
+
res.status(500).json({ error: err.message })
|
|
160
|
+
}
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
router.post('/api/limits/:name/reset/:key', async (req, res) => {
|
|
164
|
+
const limiter = limit[req.params.name]
|
|
165
|
+
if (!limiter) return res.status(404).json({ error: 'Limiter not found' })
|
|
166
|
+
if (!limiter.reset) return res.status(400).json({ error: 'Limiter does not support reset' })
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
await limiter.reset(req.params.key)
|
|
170
|
+
res.json({ ok: true })
|
|
171
|
+
} catch (err) {
|
|
172
|
+
res.status(500).json({ error: err.message })
|
|
173
|
+
}
|
|
112
174
|
})
|
|
113
175
|
}
|
|
114
176
|
|
|
@@ -152,6 +214,47 @@ export function prsmDevtools(options = {}) {
|
|
|
152
214
|
if (!execution) return res.status(404).json({ error: 'Execution not found' })
|
|
153
215
|
res.json({ execution })
|
|
154
216
|
})
|
|
217
|
+
|
|
218
|
+
router.post('/api/workflow/start', jsonBody, async (req, res) => {
|
|
219
|
+
const { name, version, input } = req.body || {}
|
|
220
|
+
if (!name || typeof name !== 'string') {
|
|
221
|
+
return res.status(400).json({ error: 'name is required' })
|
|
222
|
+
}
|
|
223
|
+
try {
|
|
224
|
+
const execution = await workflow.start(name, input ?? {}, version ? { version } : undefined)
|
|
225
|
+
res.json({ id: execution.id })
|
|
226
|
+
} catch (err) {
|
|
227
|
+
res.status(400).json({ error: err.message })
|
|
228
|
+
}
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
router.post('/api/workflow/executions/:id/signal', jsonBody, async (req, res) => {
|
|
232
|
+
try {
|
|
233
|
+
const execution = await workflow.signal(req.params.id, req.body?.payload ?? {})
|
|
234
|
+
res.json({ execution })
|
|
235
|
+
} catch (err) {
|
|
236
|
+
const status = err.name === 'AlreadySignaledError' ? 409 : 400
|
|
237
|
+
res.status(status).json({ error: err.message })
|
|
238
|
+
}
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
router.post('/api/workflow/executions/:id/cancel', jsonBody, async (req, res) => {
|
|
242
|
+
try {
|
|
243
|
+
const execution = await workflow.cancel(req.params.id, req.body?.reason)
|
|
244
|
+
res.json({ execution })
|
|
245
|
+
} catch (err) {
|
|
246
|
+
res.status(400).json({ error: err.message })
|
|
247
|
+
}
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
router.post('/api/workflow/executions/:id/resume', async (req, res) => {
|
|
251
|
+
try {
|
|
252
|
+
const execution = await workflow.resume(req.params.id)
|
|
253
|
+
res.json({ execution })
|
|
254
|
+
} catch (err) {
|
|
255
|
+
res.status(400).json({ error: err.message })
|
|
256
|
+
}
|
|
257
|
+
})
|
|
155
258
|
}
|
|
156
259
|
|
|
157
260
|
if (realtime) {
|
|
@@ -326,10 +429,85 @@ export function prsmDevtools(options = {}) {
|
|
|
326
429
|
})
|
|
327
430
|
}
|
|
328
431
|
|
|
432
|
+
if (cellGraphs) {
|
|
433
|
+
router.get('/api/cells/:graph', (req, res) => {
|
|
434
|
+
const g = cellGraphs[req.params.graph]
|
|
435
|
+
if (!g) return res.status(404).json({ error: 'graph not found' })
|
|
436
|
+
const topology = g.cells()
|
|
437
|
+
const snapshot = g.snapshot()
|
|
438
|
+
const enriched = topology.map((c) => {
|
|
439
|
+
const state = g.get(c.name)
|
|
440
|
+
return {
|
|
441
|
+
...c,
|
|
442
|
+
value: snapshot[c.name],
|
|
443
|
+
error: state?.error ? String(state.error.message || state.error) : null,
|
|
444
|
+
updatedAt: state?.updatedAt ?? null,
|
|
445
|
+
computeTime: state?.computeTime ?? null,
|
|
446
|
+
}
|
|
447
|
+
})
|
|
448
|
+
res.json({ graph: req.params.graph, cells: enriched })
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
router.get('/api/cells/:graph/:name/history', (req, res) => {
|
|
452
|
+
const g = cellGraphs[req.params.graph]
|
|
453
|
+
if (!g) return res.status(404).json({ error: 'graph not found' })
|
|
454
|
+
const limit = req.query.limit ? Number(req.query.limit) : undefined
|
|
455
|
+
try {
|
|
456
|
+
const entries = g.history(req.params.name, limit)
|
|
457
|
+
res.json({ graph: req.params.graph, name: req.params.name, entries })
|
|
458
|
+
} catch (err) {
|
|
459
|
+
res.status(500).json({ error: err.message })
|
|
460
|
+
}
|
|
461
|
+
})
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (lock) {
|
|
465
|
+
router.get('/api/locks', async (_req, res) => {
|
|
466
|
+
const managers = []
|
|
467
|
+
for (const [name, manager] of Object.entries(lock)) {
|
|
468
|
+
const kind = typeof manager.renew === 'function' ? 'semaphore' : 'mutex'
|
|
469
|
+
try {
|
|
470
|
+
managers.push({ name, kind, locks: await manager.list() })
|
|
471
|
+
} catch (err) {
|
|
472
|
+
managers.push({ name, kind, locks: [], error: err.message })
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
res.json({ managers })
|
|
476
|
+
})
|
|
477
|
+
|
|
478
|
+
router.get('/api/locks/:name', async (req, res) => {
|
|
479
|
+
const manager = lock[req.params.name]
|
|
480
|
+
if (!manager) return res.status(404).json({ error: 'Lock manager not found' })
|
|
481
|
+
try {
|
|
482
|
+
const kind = typeof manager.renew === 'function' ? 'semaphore' : 'mutex'
|
|
483
|
+
const locks = await manager.list()
|
|
484
|
+
res.json({ name: req.params.name, kind, locks })
|
|
485
|
+
} catch (err) {
|
|
486
|
+
res.status(500).json({ error: err.message })
|
|
487
|
+
}
|
|
488
|
+
})
|
|
489
|
+
|
|
490
|
+
router.post('/api/locks/:name/release', jsonBody, async (req, res) => {
|
|
491
|
+
const manager = lock[req.params.name]
|
|
492
|
+
if (!manager) return res.status(404).json({ error: 'Lock manager not found' })
|
|
493
|
+
const { key, id } = req.body || {}
|
|
494
|
+
if (!key || !id) return res.status(400).json({ error: 'key and id are required' })
|
|
495
|
+
try {
|
|
496
|
+
const released = await manager.release(key, id)
|
|
497
|
+
res.json({ released })
|
|
498
|
+
} catch (err) {
|
|
499
|
+
res.status(500).json({ error: err.message })
|
|
500
|
+
}
|
|
501
|
+
})
|
|
502
|
+
}
|
|
503
|
+
|
|
329
504
|
if (existsSync(clientDir)) {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
505
|
+
const indexPath = resolve(clientDir, 'index.html')
|
|
506
|
+
const indexHtml = readFileSync(indexPath, 'utf8')
|
|
507
|
+
router.use(serveStatic(clientDir, { index: false }))
|
|
508
|
+
router.get('*', (req, res) => {
|
|
509
|
+
const base = req.baseUrl ? `${req.baseUrl}/` : '/'
|
|
510
|
+
res.type('html').send(indexHtml.replace('<head>', `<head>\n <base href="${base}">`))
|
|
333
511
|
})
|
|
334
512
|
}
|
|
335
513
|
|