Skip to content

feat(display/http): Angular SSR through a resident engine/ts context#41

Merged
Snider merged 1 commit into
mainfrom
lane/angular-ssr-ts
Jul 24, 2026
Merged

feat(display/http): Angular SSR through a resident engine/ts context#41
Snider merged 1 commit into
mainfrom
lane/angular-ssr-ts

Conversation

@Snider

@Snider Snider commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Serve an Angular app with SSR via the Go-native CoreTS engine — Angular's non-Node SSR contract (RequestHandlerFunction: (Request) => Response | null | Promise), no Node.js sidecar. Foundation for framework/react/ etc.

What landed

  • engine/tsEngine.Load + persistent Context.Invoke: load a server bundle once, call an exported function per request. Retained module state, JSON arg/result marshalling, lifecycle cleanup, and Web Request/Response bridging (web.go).
  • display/http/framework — shared Loader/Renderer/Response boundary so framework/<name>/ slot in behind one contract.
  • display/http/framework/angular — the adapter: loads reqHandler once, translates the HTTP request (host, headers, body) → a Web Request, maps the returned Response (status, repeated headers, body, null → 404).
  • display/httpWithFramework mount option; static assets + legacy renderer fallback preserved.

Acceptance proof

framework/angular/server_example_test.go consumes the adapter as a real caller — retained state across requests, null → 404:

201 1 <main>1 GET /first</main>
201 2 <main>2 GET /second</main>
404  404 page not found

Gate

GOWORK=off go build ./... ✓ · go vet ./engine/ts/... ./display/http/... ✓ · 33 tests green across engine/ts, display/http/framework, framework/angular.

Refs: Angular non-Node SSR, RequestHandlerFunction.

🤖 Generated with Claude Code
Co-Authored-By: Virgil virgil@lethean.io

Summary by CodeRabbit

  • New Features

    • Added support for Angular server-side rendering through the HTTP handler.
    • Added a framework renderer interface for integrating alternative server-rendering frameworks.
    • Framework-rendered responses now support status codes, headers, request bodies and response bodies.
    • Unhandled framework routes fall through correctly and return 404 responses where appropriate.
    • Added support for persistent TypeScript rendering contexts, improving repeated request handling.
  • Bug Fixes

    • Improved request validation, error handling and renderer shutdown behaviour.
    • HEAD requests no longer include response bodies.

Serve an Angular app with server-side rendering via the Go-native CoreTS
engine — Angular's non-Node SSR contract (RequestHandlerFunction:
(Request) => Response | null | Promise) with no Node.js sidecar.

- engine/ts: add Engine.Load + persistent Context.Invoke (load a bundle
  once, call an exported function per request), retained module state,
  JSON arg/result marshalling, lifecycle cleanup, and Web Request/Response
  bridging (web.go).
- display/http/framework: shared Loader/Renderer/Response boundary so
  framework/<name>/ (react, …) slot in later behind one contract.
- display/http/framework/angular: the adapter — loads reqHandler once,
  translates the HTTP request (host, headers, body) to a Web Request,
  maps the returned Response (status, repeated headers, body, null → 404).
- display/http: WithFramework mount option, static assets + legacy
  renderer fallback preserved.
- Acceptance: server_example_test.go proves retained state across requests
  and null → 404 when consumed as a real caller.

Co-Authored-By: Virgil <virgil@lethean.io>
@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

Server-side rendering integration

Layer / File(s) Summary
Framework and web contracts
go/display/http/framework/..., go/engine/ts/web.go, related examples and tests
Defines renderer, loader, response, web request, and web response contracts.
Resident TypeScript context transport
go/engine/ts/context.go, related tests and examples
Adds persistent JSON-RPC invocation, request/response conversion, wrapper generation, and context cleanup.
Engine context lifecycle
go/engine/ts/engine.go, go/engine/ts/engine_test.go
Tracks resident contexts, closes them with the engine, improves entry-point errors, and updates worker lifecycle handling.
HTTP framework dispatch
go/display/http/http.go, go/display/http/http_test.go
Adds WithFramework, framework-first request handling, response writing, and related tests.
Angular SSR adapter and validation
go/display/http/framework/angular/...
Loads Angular bundles, converts HTTP requests, invokes reqHandler, maps responses, and tests sidecar and lifecycle behaviour.

Sequence Diagram(s)

sequenceDiagram
  participant HTTPClient
  participant HTTPHandler
  participant AngularRenderer
  participant ResidentContext
  participant AngularReqHandler
  HTTPClient->>HTTPHandler: Send HTTP request
  HTTPHandler->>AngularRenderer: Render request
  AngularRenderer->>ResidentContext: Invoke reqHandler
  ResidentContext->>AngularReqHandler: Dispatch WebRequest
  AngularReqHandler-->>ResidentContext: Return WebResponse or nil
  ResidentContext-->>AngularRenderer: Decode response
  AngularRenderer-->>HTTPHandler: Return framework Response
  HTTPHandler-->>HTTPClient: Write HTTP response
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.76% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: Angular SSR via a resident engine/ts context in display/http.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.2)

Error: can't load config: unsupported version of the configuration: "" See https://golangci-lint.run/docs/product/migration-guide for migration instructions
The command is terminated due to an error: can't load config: unsupported version of the configuration: "" See https://golangci-lint.run/docs/product/migration-guide for migration instructions

Warning

Billing warning: we have not been able to collect payment for this subscription for more than 72 hours. Please update the payment method or pay any pending invoices in Billing to avoid service interruption.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@sonarqubecloud

Copy link
Copy Markdown

@Snider
Snider merged commit a82e538 into main Jul 24, 2026
2 of 4 checks passed
@Snider
Snider deleted the lane/angular-ssr-ts branch July 24, 2026 10:54

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (1)
go/display/http/framework/angular/server.go (1)

70-97: 🚀 Performance & Scalability | 🔵 Trivial | ⚖️ Poor tradeoff

Throughput note: r.mu is held across the blocking Invoke.

Holding r.mu for the full webRequestFromHTTP + r.module.Invoke path serialises every SSR render through this renderer and also blocks Close until an in-flight render completes. Combined with the single resident connection (each Invoke is already serialised by callMu), one slow render stalls all others. If concurrent SSR throughput becomes a requirement, consider a pool of resident contexts or narrowing the lock to the closed/module snapshot only.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@go/display/http/framework/angular/server.go` around lines 70 - 97, The Render
method currently holds r.mu across webRequestFromHTTP and the blocking
r.module.Invoke call, serializing renders and delaying Close. Narrow the lock to
snapshot and validate r.closed and r.module, then release it before request
conversion and Invoke; preserve the closed-renderer error and use the captured
module for the invocation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@go/display/http/framework/framework_test.go`:
- Around line 31-52: Add Good/Bad/Ugly triplet tests for the public Loader,
Renderer, and Response contracts in go/display/http/framework/framework_test.go,
using the existing TestFramework_Contracts and TestFramework_Response coverage
as context. Also add Good/Bad/Ugly triplets for WebRequest and WebResponse in
go/engine/ts/web_test.go, naming them TestWeb_WebRequest_{Good,Bad,Ugly} and
TestWeb_WebResponse_{Good,Bad,Ugly} and covering each contract’s relevant valid,
invalid, and edge behaviors.

In `@go/display/http/http_example_test.go`:
- Around line 15-17: Update ExampleWithFramework to invoke WithFramework with a
representative usage scenario and add an // Output: block containing the
deterministic expected output, so the example is executed and validated by Go’s
example tests.

In `@go/engine/ts/context.go`:
- Around line 290-301: Set a read deadline on each accepted resident connection
before reader.ReadString('\n') in loadContext, reusing its existing deadline
value; also update Invoke to derive and set a connection read deadline from ctx
or the call timeout before its blocking read. Preserve the existing cleanup and
error handling while ensuring both read paths cannot block indefinitely.
- Around line 133-149: Deregister each closed Context from the owning Engine’s
contexts map during the normal context-close path, while avoiding removal inside
Context.close() because Engine.Close invokes it under Engine.mu. Add a separate
Engine.mu-safe cleanup path that removes the matching context after close, and
ensure repeated Load/Close cycles do not retain closed *Context references.

---

Nitpick comments:
In `@go/display/http/framework/angular/server.go`:
- Around line 70-97: The Render method currently holds r.mu across
webRequestFromHTTP and the blocking r.module.Invoke call, serializing renders
and delaying Close. Narrow the lock to snapshot and validate r.closed and
r.module, then release it before request conversion and Invoke; preserve the
closed-renderer error and use the captured module for the invocation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d6b1dd9b-bac0-4597-95a2-95ea1f8a3564

📥 Commits

Reviewing files that changed from the base of the PR and between 3d081e9 and bba2e92.

📒 Files selected for processing (18)
  • go/display/http/framework/angular/server.go
  • go/display/http/framework/angular/server_example_test.go
  • go/display/http/framework/angular/server_test.go
  • go/display/http/framework/framework.go
  • go/display/http/framework/framework_example_test.go
  • go/display/http/framework/framework_test.go
  • go/display/http/http.go
  • go/display/http/http_example_test.go
  • go/display/http/http_test.go
  • go/engine/ts/context.go
  • go/engine/ts/context_example_test.go
  • go/engine/ts/context_test.go
  • go/engine/ts/engine.go
  • go/engine/ts/engine_example_test.go
  • go/engine/ts/engine_test.go
  • go/engine/ts/web.go
  • go/engine/ts/web_example_test.go
  • go/engine/ts/web_test.go

Comment on lines +31 to +52
func TestFramework_Contracts(t *testing.T) {
var loader Loader = loaderContract{}
var renderer Renderer = rendererContract{}
context, err := loader.Load(core.Background(), "server.mjs")
core.AssertNoError(t, err)
core.AssertNotNil(t, context)
response, err := renderer.Render(core.Background(), &nethttp.Request{})
core.AssertNoError(t, err)
core.AssertEqual(t, nethttp.StatusNoContent, response.Status)
core.AssertNoError(t, renderer.Close())
}

func TestFramework_Response(t *testing.T) {
response := Response{
Status: nethttp.StatusAccepted,
Header: nethttp.Header{"X-Framework": {"Angular"}},
Body: []byte("<main>accepted</main>"),
}
core.AssertEqual(t, nethttp.StatusAccepted, response.Status)
core.AssertEqual(t, "Angular", response.Header.Get("X-Framework"))
core.AssertEqual(t, []byte("<main>accepted</main>"), response.Body)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add the required Good/Bad/Ugly test triplets for new public contracts.

  • go/display/http/framework/framework_test.go#L31-L52: add TestFramework_Loader_{Good,Bad,Ugly}, TestFramework_Renderer_{Good,Bad,Ugly}, and TestFramework_Response_{Good,Bad,Ugly} coverage.
  • go/engine/ts/web_test.go#L13-L37: add TestWeb_WebRequest_{Good,Bad,Ugly} and TestWeb_WebResponse_{Good,Bad,Ugly} coverage.

As per coding guidelines, “Public symbols in Go files must have a corresponding TestFoo_<Symbol>_{Good,Bad,Ugly} triplet test in foo_test.go”.

📍 Affects 2 files
  • go/display/http/framework/framework_test.go#L31-L52 (this comment)
  • go/engine/ts/web_test.go#L13-L37
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@go/display/http/framework/framework_test.go` around lines 31 - 52, Add
Good/Bad/Ugly triplet tests for the public Loader, Renderer, and Response
contracts in go/display/http/framework/framework_test.go, using the existing
TestFramework_Contracts and TestFramework_Response coverage as context. Also add
Good/Bad/Ugly triplets for WebRequest and WebResponse in
go/engine/ts/web_test.go, naming them TestWeb_WebRequest_{Good,Bad,Ugly} and
TestWeb_WebResponse_{Good,Bad,Ugly} and covering each contract’s relevant valid,
invalid, and edge behaviors.

Source: Coding guidelines

Comment on lines +15 to +17
func ExampleWithFramework() {
_ = WithFramework
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Make the WithFramework example executable.

It neither invokes WithFramework nor has an // Output: block, so it is not a runnable usage example.

Proposed fix
+import core "dappco.re/go"
+
 func ExampleWithFramework() {
-	_ = WithFramework
+	option := WithFramework(nil)
+	core.Println(option != nil)
+	// Output: true
 }

As per coding guidelines, “Add an Example with an // Output: block for every public feature.”

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func ExampleWithFramework() {
_ = WithFramework
}
import core "dappco.re/go"
func ExampleWithFramework() {
option := WithFramework(nil)
core.Println(option != nil)
// Output: true
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@go/display/http/http_example_test.go` around lines 15 - 17, Update
ExampleWithFramework to invoke WithFramework with a representative usage
scenario and add an // Output: block containing the deterministic expected
output, so the example is executed and validated by Go’s example tests.

Source: Coding guidelines

Comment thread go/engine/ts/context.go
Comment on lines +133 to +149
func (c *Context) close() error {
c.stateMu.Lock()
if c.closed {
err := c.closeErr
c.stateMu.Unlock()
return err
}
c.closed = true
connection := c.conn
service := c.service
identity := c.identity
workspace := c.workspace
c.conn = nil
c.reader = nil
c.service = nil
c.workspace = ""
c.stateMu.Unlock()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate file =="
git ls-files | rg '(^|/)go/engine/ts/context\.go$|engine|ts/context' || true

echo "== outline =="
ast-grep outline go/engine/ts/context.go --view compact || true

echo "== relevant loadContext/close/open sections =="
sed -n '280,360p' go/engine/ts/context.go
echo "---"
sed -n '120,175p' go/engine/ts/context.go

echo "== contexts usages =="
rg -n "contexts|loadContext|\.close\(|Close\(" go/engine/ts/context.go

Repository: dAppCore/go-render

Length of output: 11183


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Engine methods outline =="
ast-grep outline go/engine/ts/engine.go --view digest || true

echo "== Engine.go relevant sections =="
wc -l go/engine/ts/engine.go
sed -n '1,220p' go/engine/ts/engine.go
echo "--- contexts/delete references across ts package =="
rg -n "contexts|delete\(.*contexts|\.close\(" go/engine/ts -g '*.go'

Repository: dAppCore/go-render

Length of output: 8994


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Engine.Close tail =="
sed -n '206,245p' go/engine/ts/engine.go

echo "== Test failure for repeated Load/Close =="
sed -n '220,260p' go/engine/ts/engine_test.go

echo "== Read-only lock-ordering model probe =="
python3 - <<'PY'
from pathlib import Path
def extract(block, pattern=''):
    lines=open(block).read().splitlines()
    for i,l in enumerate(lines,1):
        if pattern in l or 'for context := range' in l:
            print(f'{block}:{i}: {l}')
    seg=lines[0]
    for n,line in enumerate(lines):
        if f'func {seg}' in line:
            for row in lines[n:min(n+35,len(lines))]:
                print(f'{block}:{n+1}: {row}')

extract('go/engine/ts/context.go', 'contexts')
extract('go/engine/ts/engine.go', 'contexts')
print('Engine.Lock before iteration and nulls after context.close')
print('Context.Close only owns c.stateMu; no Engine.mu acquisition in extracted context.close')
print('No e.contexts entry removal in extracted context.go (except e.contexts = nil in Engine.Close)')
PY

Repository: dAppCore/go-render

Length of output: 2968


Deregister Context entries when closed

loadContext stores the new resident Context in e.contexts, but Context.close() only clears the context’s fields and never removes the map entry. Engine.Close is the only path that resets e.contexts, so repeated Load/Close cycles on the same engine leak closed *Context references. Moving the map removal into close() is not safe because Engine.Close already calls context.close() while holding Engine.mu; the deregistration must be done on a separate Engine.mu-safe path instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@go/engine/ts/context.go` around lines 133 - 149, Deregister each closed
Context from the owning Engine’s contexts map during the normal context-close
path, while avoiding removal inside Context.close() because Engine.Close invokes
it under Engine.mu. Add a separate Engine.mu-safe cleanup path that removes the
matching context after close, and ensure repeated Load/Close cycles do not
retain closed *Context references.

Comment thread go/engine/ts/context.go
Comment on lines +290 to +301
connection, err := listener.Accept()
if err != nil {
return nil, core.E("ts.Engine.Load", "accept resident call connection", err)
}
reader := core.NewBufReader(connection)
line, err := reader.ReadString('\n')
if err != nil {
if closeErr := connection.Close(); closeErr != nil {
core.Warn("TypeScript resident connection cleanup failed", "err", closeErr)
}
return nil, core.E("ts.Engine.Load", "read resident ready frame", err)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Files:"
git ls-files | rg '^go/engine/ts/context\.go$' || true

echo
echo "Outline:"
ast-grep outline go/engine/ts/context.go || true

echo
echo "Relevant sections:"
wc -l go/engine/ts/context.go
sed -n '1,180p' go/engine/ts/context.go
sed -n '250,320p' go/engine/ts/context.go

Repository: dAppCore/go-render

Length of output: 8227


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Top-level imports/constants/variables:"
sed -n '1,40p' go/engine/ts/context.go

echo
echo "Engine loadContext complete:"
sed -n '187,335p' go/engine/ts/context.go

echo
echo "Search for deadlines and Conn operations in file:"
rg -n "SetDeadline|NewBuffer|NewBufReader|ReadString|ctx\.Err|connection|c\.conn|stateMu|callMu" go/engine/ts/context.go

Repository: dAppCore/go-render

Length of output: 7946


Set read deadlines on accepted resident connections. The listener deadline does not carry over to either read path, so Invoke and loadContext can block indefinitely on reader.ReadString('\n'); Invoke also ignores ctx.Err() until after the read returns. Set a connection read deadline before each blocking read: reuse deadline for loadContext, and derive one from ctx/call timeout for Invoke.

🧰 Tools
🪛 GitHub Check: SonarCloud Code Analysis

[failure] 298-298: Define a constant instead of duplicating this literal "TypeScript resident connection cleanup failed" 3 times.

See more on https://sonarcloud.io/project/issues?id=dAppCore_go-html&issues=AZ-TweO4tLtWNqPa6wtm&open=AZ-TweO4tLtWNqPa6wtm&pullRequest=41

📍 Affects 1 file
  • go/engine/ts/context.go#L290-L301 (this comment)
  • go/engine/ts/context.go#L91-L97
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@go/engine/ts/context.go` around lines 290 - 301, Set a read deadline on each
accepted resident connection before reader.ReadString('\n') in loadContext,
reusing its existing deadline value; also update Invoke to derive and set a
connection read deadline from ctx or the call timeout before its blocking read.
Preserve the existing cleanup and error handling while ensuring both read paths
cannot block indefinitely.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant