Skip to content

feat: explorer web live refresh - #1698

Merged
megha-narayanan merged 9 commits into
aws:feat/cdk-explorerfrom
megha-narayanan:feat/explorer-web-live-refresh
Jul 8, 2026
Merged

feat: explorer web live refresh#1698
megha-narayanan merged 9 commits into
aws:feat/cdk-explorerfrom
megha-narayanan:feat/explorer-web-live-refresh

Conversation

@megha-narayanan

@megha-narayanan megha-narayanan commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Adds live refresh to the cdk explore web UI. When the cloud assembly on disk changes, the server pushes an event and the SPA re-fetches, so the construct tree and violations stay current without a manual reload.

  • SSE stream (GET /api/events): a long-lived server-sent events channel. SseBroadcaster tracks connected clients and pushes to all of them, evicting any that disconnect.
  • cdk.out watcher: same watcher as LSP
  • SPA: subscribes on load and re-fetches the tree and violations on each event.
  • The event carries no payload. The server holds no assembly state, so the client just re-reads /api/tree and /api/policy-validation on receipt. Also narrows the web routes' injectable readAssembly seam to Promise to match the real reader.

Checklist

  • This change contains a major version upgrade for a dependency and I confirm all breaking changes are addressed
    • Release notes for the new version:

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license

The path-containment merge makes readAssembly async (buildConstructTreeAsync). Await it in the /tree and /policy-validation handlers and narrow the ApiOptions.readAssembly seam to Promise so the request path no longer blocks on a synchronous read.
Server-side SSE hub: tracks connected browsers and broadcasts a content-free assembly-changed signal so clients re-fetch through the existing GET endpoints. Evicts a client on request close or socket error so a vanished client is never written to.
startWebServer resolves the assembly dir once and shares it with the read endpoints and the watcher, registers GET /api/events (SseBroadcaster), and starts the assembly watcher whose onChange broadcasts an assembly-changed signal. stop() tears down the watcher then the broadcaster then the server.
The SPA opens an EventSource to /api/events and re-fetches the tree and violations on each assembly-changed signal, keeping the last good render on a transient read. The event name lives in the shared protocol contract so the server broadcaster and the client subscriber share one constant.
- Type the SSE event name (SseEventName) instead of broadcast(string).
- Route watcher errors through the command IoHost (explore.ts), with safe Error extraction; stderr default in the lib.
- Document that the SSE data line is required for EventSource dispatch; drop the broadcast try/catch (a write to a dead socket returns false, never throws).
- Server integration test asserts via the shared constant.
@github-actions github-actions Bot added the p2 label Jul 1, 2026
@megha-narayanan
megha-narayanan marked this pull request as ready for review July 1, 2026 21:10
@aws-cdk-automation
aws-cdk-automation requested a review from a team July 1, 2026 21:15

@rix0rrr rix0rrr left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The event carries no payload. The server holds no assembly state, so the client just re-reads /api/tree and /api/policy-validation on receipt.

Nice! Exactly what I would have done

// Live-refresh stream: browsers subscribe here and re-fetch when the assembly
// changes. Registered before the /api catch-all so it is not treated as unknown.
const events = new SseBroadcaster();
app.get('/api/events', events.handle);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This only works if events.handle doesn't use any this references.

It's a bit hard to explain and different from all other programming languages, but this is dynamically bound in JavaScript, and it only gets a value at the exact moment when you call a function.

If you "take" a function from an object and pass it around, this is unbound and when you call a function without an object it gets bound to the global object.

Now, it might be that that's not a problem because events.handle is a closure that never uses this, but I can't tell that from this call site, so this looks supicious. As a rule:

I would like to be able to reason about things locally. If they look locally correct, they should be globally correct. This might still be globally correct, but it looks locally incorrect, so breaks my heuristics.

Use the following instead:

// This bakes the value of "this" into the function
app.get('/api/events', events.handle.bind(events));

I thought we had a linter rule for this... 🤔

@megha-narayanan megha-narayanan Jul 7, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed, handle is now a normal method and I bind it at the call site: app.get('/api/events', events.handle.bind(events)). I didn't see a linter rule but I do agree anyway.

url: `http://${host}:${port}`,
stop: () => {
if (stopped) return Promise.resolve();
url: `http://127.0.0.1:${port}`,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use localhost instead. On some operating systems it uses a faster network path (that skips a few layers), and it can't be wrong even if IPv4 is turned off.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also -- why hardcode this in a lot of places? Why not pass our preferred value in the host as an argument, and keep it deduplicated and easy to change?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will fix to be localhost instead. I was trying to clean up options that we didn't need/ never used, but I can absolutely make this an argument as localhost to make it easy to change.

* registers the client, removing it when the request closes or the socket
* errors so a vanished client is never written to.
*/
public readonly handle = (req: Request, res: Response): void => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just function syntax?

public handle(req: Request, res: Response): void {
  // ...
}

I think I have this comment in a lot of places, where you use function expression syntax instead of function declaration syntax. I personally find function declaration syntax a lot easier to grok, and I'd prefer we use that as much as possible.


To be frank I'm not even sure whether this binds the this that we think it does. I guess it has to because otherwise TypeScript would complain?

Hmm, thinking about how this must work, I think I see why this gets bound correctly and you also don't need the .bind() if you write it like this.

Still, when I look at the usage site I don't know any of that, so it still looks sus.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Switched to function-declaration syntax. Now that it's a real method I bind it explicitly at the call site (see the /api/events thread).

Make SseBroadcaster.handle a function-declaration method instead of an
arrow class field, and bind it where it is registered on /api/events so
this is preserved and the call site is correct on its own. Addresses PR
review feedback.
@megha-narayanan
megha-narayanan merged commit e26222a into aws:feat/cdk-explorer Jul 8, 2026
7 of 8 checks passed
@megha-narayanan
megha-narayanan deleted the feat/explorer-web-live-refresh branch July 8, 2026 17:55
megha-narayanan added a commit to megha-narayanan/aws-cdk-cli that referenced this pull request Jul 8, 2026
…and template

Rebased aws#1706 onto the updated feat/cdk-explorer (now carrying feat/cdk-lsp)
as a single integration commit. Adds source/template/tree navigation with
syntax highlighting, a YAML template view, and a file picker, and serves each
assembly read under the Toolkit read lock via the factory core extracted in
aws#1715. Coexists with the SSE live-refresh from aws#1698: the reload effect and
the navigation state share the same App shell.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants