Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 22 additions & 2 deletions cmd/tinkerdown/commands/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,11 +171,25 @@ func validateMermaidDiagrams(filePath string) ([]string, error) {

var errors []string

// Create chrome context
// Create chrome context.
//
// --disable-dev-shm-usage is the critical flag on Ubuntu CI runners:
// the default /dev/shm is 64MB on Docker/Actions runners, Chrome's
// renderer OOMs trying to allocate shared memory there, and the
// fallback path manifests as "chrome failed to start: Failed to
// connect to the bus" (D-Bus negotiation failure after the shm OOM).
// Switching to /tmp via --disable-dev-shm-usage avoids both.
//
// --disable-extensions and --no-first-run shave a second or two off
// cold-start by skipping the default extension scan and welcome
// flow Chromium does on a fresh profile.
opts := append(chromedp.DefaultExecAllocatorOptions[:],
chromedp.Flag("headless", true),
chromedp.Flag("disable-gpu", true),
chromedp.Flag("no-sandbox", true),
chromedp.Flag("disable-dev-shm-usage", true),
chromedp.Flag("disable-extensions", true),
chromedp.Flag("no-first-run", true),
)

allocCtx, cancel := chromedp.NewExecAllocator(context.Background(), opts...)
Expand All @@ -184,7 +198,13 @@ func validateMermaidDiagrams(filePath string) ([]string, error) {
ctx, cancel := chromedp.NewContext(allocCtx)
defer cancel()

ctx, cancel = context.WithTimeout(ctx, 15*time.Second)
// Per-FILE deadline. Chrome cold-start can take 5-10s on a slow Ubuntu
// CI runner, and each diagram below adds Navigate + Sleep(2s) +
// Evaluate (~3s). 60s comfortably fits the worst case observed on CI
// (cold Chrome + several diagrams + occasional D-Bus init jitter)
// without masking real hangs. The previous 15s budget was tight on
// devbox and routinely missed on CI.
ctx, cancel = context.WithTimeout(ctx, 60*time.Second)
defer cancel()

// Create a simple HTML page with Mermaid
Expand Down
47 changes: 39 additions & 8 deletions examples/literate-counter-include/_app/counter.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,26 +10,57 @@ type Counter struct {
// CounterController holds dependencies (none in this demo).
type CounterController struct{}

// Mount opts this connection in to peer fan-out for the session via
// ctx.Subscribe(ctx.SelfTopic()). Without this opt-in, an Increment in
// one tab's Publish would have no peer subscribers and the embeds would
// drift. Subscribe is idempotent per-connection, so re-Mounts (which
// happen on every HTTP request and WS connect) are no-ops.
//
// We propagate the error rather than silently discarding it: SelfTopic()
// is ACL-exempt in livetemplate v0.10.0, but a controller that copies
// this pattern with a developer topic name MUST propagate the error to
// trigger the keep-open lvt:error envelope path. Keeping the propagation
// here makes the example safe-by-default for readers.
func (c *CounterController) Mount(s Counter, ctx *livetemplate.Context) (Counter, error) {
if err := ctx.Subscribe(ctx.SelfTopic()); err != nil {
return s, err
}
return s, nil
}

// Increment is the action handler invoked when the user clicks the
// "+" button. Mutate the cloned state, then broadcast the same action
// to every *other* connected client so multiple embeds (and tabs)
// stay in sync. Broadcasts triggered by a broadcast are no-ops, so
// no infinite loop.
// "+" button. Mutate the cloned state, then publish the same action
// to every *other* subscribed connection so multiple embeds (and tabs)
// stay in sync. Publishes triggered by a dispatched action are no-ops
// (the framework's recursion guard), so no infinite loop.
//
// Mutate-first ordering is safe even when Publish errors: the
// livetemplate dispatcher only assigns the returned newState to the
// connection's persisted state when the action returns (state, nil).
// On (state, err), newState is discarded — both this connection AND
// the peer connections that never received the failed Publish stay at
// the pre-action state. No divergence by construction.
func (c *CounterController) Increment(s Counter, ctx *livetemplate.Context) (Counter, error) {
s.Count++
ctx.BroadcastAction("Increment", nil)
if err := ctx.Publish(ctx.SelfTopic(), "Increment", nil); err != nil {
return s, err
}
return s, nil
}

// Decrement and Reset follow the same pattern: mutate, broadcast.
// Decrement and Reset follow the same pattern: mutate, publish.
func (c *CounterController) Decrement(s Counter, ctx *livetemplate.Context) (Counter, error) {
s.Count--
ctx.BroadcastAction("Decrement", nil)
if err := ctx.Publish(ctx.SelfTopic(), "Decrement", nil); err != nil {
return s, err
}
return s, nil
}

func (c *CounterController) Reset(s Counter, ctx *livetemplate.Context) (Counter, error) {
s.Count = 0
ctx.BroadcastAction("Reset", nil)
if err := ctx.Publish(ctx.SelfTopic(), "Reset", nil); err != nil {
return s, err
}
return s, nil
}
8 changes: 4 additions & 4 deletions examples/literate-counter-include/_app/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ import (
)

// sharedAuth puts every connection in the same session group, so
// `ctx.BroadcastAction` from any tab/embed reaches everyone else.
// Real apps use a per-user authenticator; here a constant groupID is
// what makes the literate-linked tutorial demonstrate cross-region
// state sharing.
// `ctx.Publish(ctx.SelfTopic(), ...)` from any tab/embed reaches every
// other subscribed peer. Real apps use a per-user authenticator; the
// constant groupID here is what makes the literate-linked tutorial
// demonstrate cross-region state sharing.
type sharedAuth struct{}

func (sharedAuth) Identify(r *http.Request) (string, error) {
Expand Down
8 changes: 5 additions & 3 deletions examples/literate-counter-include/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,12 @@ clones it per session, so two readers don't see each other's count.
Action handlers are methods on a controller. When the reader clicks
`+`, the runtime calls `Increment` with a clone of the current state
and stores whatever you return. `Decrement` and `Reset` follow the
same pattern. Each handler also calls `ctx.BroadcastAction(...)` so
every connected embed and direct visitor stays in lockstep.
same pattern. `Mount` opts each connection in to peer fan-out via
`ctx.Subscribe(ctx.SelfTopic())`; each action handler then calls
`ctx.Publish(ctx.SelfTopic(), ...)` so every connected embed and
direct visitor stays in lockstep.

```go include="./_app/counter.go" lines="13-35" highlight="20"
```go include="./_app/counter.go" lines="13-66" highlight="45"
```

## The template
Expand Down
47 changes: 39 additions & 8 deletions examples/literate-linked-include/_app/counter.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,26 +10,57 @@ type Counter struct {
// CounterController holds dependencies (none in this demo).
type CounterController struct{}

// Mount opts this connection in to peer fan-out for the session via
// ctx.Subscribe(ctx.SelfTopic()). Without this opt-in, an Increment in
// one tab's Publish would have no peer subscribers and the embeds would
// drift. Subscribe is idempotent per-connection, so re-Mounts (which
// happen on every HTTP request and WS connect) are no-ops.
//
// We propagate the error rather than silently discarding it: SelfTopic()
// is ACL-exempt in livetemplate v0.10.0, but a controller that copies
// this pattern with a developer topic name MUST propagate the error to
// trigger the keep-open lvt:error envelope path. Keeping the propagation
// here makes the example safe-by-default for readers.
func (c *CounterController) Mount(s Counter, ctx *livetemplate.Context) (Counter, error) {
if err := ctx.Subscribe(ctx.SelfTopic()); err != nil {
return s, err
}
return s, nil
}

// Increment is the action handler invoked when the user clicks the
// "+" button. Mutate the cloned state, then broadcast the same action
// to every *other* connected client so multiple embeds (and tabs)
// stay in sync. Broadcasts triggered by a broadcast are no-ops, so
// no infinite loop.
// "+" button. Mutate the cloned state, then publish the same action
// to every *other* subscribed connection so multiple embeds (and tabs)
// stay in sync. Publishes triggered by a dispatched action are no-ops
// (the framework's recursion guard), so no infinite loop.
//
// Mutate-first ordering is safe even when Publish errors: the
// livetemplate dispatcher only assigns the returned newState to the
// connection's persisted state when the action returns (state, nil).
// On (state, err), newState is discarded — both this connection AND
// the peer connections that never received the failed Publish stay at
// the pre-action state. No divergence by construction.
func (c *CounterController) Increment(s Counter, ctx *livetemplate.Context) (Counter, error) {
s.Count++
ctx.BroadcastAction("Increment", nil)
if err := ctx.Publish(ctx.SelfTopic(), "Increment", nil); err != nil {
return s, err
}
return s, nil
}

// Decrement and Reset follow the same pattern: mutate, broadcast.
// Decrement and Reset follow the same pattern: mutate, publish.
func (c *CounterController) Decrement(s Counter, ctx *livetemplate.Context) (Counter, error) {
s.Count--
ctx.BroadcastAction("Decrement", nil)
if err := ctx.Publish(ctx.SelfTopic(), "Decrement", nil); err != nil {
return s, err
}
return s, nil
}

func (c *CounterController) Reset(s Counter, ctx *livetemplate.Context) (Counter, error) {
s.Count = 0
ctx.BroadcastAction("Reset", nil)
if err := ctx.Publish(ctx.SelfTopic(), "Reset", nil); err != nil {
return s, err
}
return s, nil
}
8 changes: 4 additions & 4 deletions examples/literate-linked-include/_app/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ import (
)

// sharedAuth puts every connection in the same session group, so
// `ctx.BroadcastAction` from any tab/embed reaches everyone else.
// Real apps use a per-user authenticator; here a constant groupID is
// what makes the literate-linked tutorial demonstrate cross-region
// state sharing.
// `ctx.Publish(ctx.SelfTopic(), ...)` from any tab/embed reaches every
// other subscribed peer. Real apps use a per-user authenticator; the
// constant groupID here is what makes the literate-linked tutorial
// demonstrate cross-region state sharing.
type sharedAuth struct{}

func (sharedAuth) Identify(r *http.Request) (string, error) {
Expand Down
14 changes: 8 additions & 6 deletions examples/literate-linked-include/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ runs:
```go include="./_app/counter.go" lines="5-8"
```

```go include="./_app/counter.go" lines="13-35"
```go include="./_app/counter.go" lines="13-66"
```

```html include="./_app/counter.tmpl" lines="10-13"
Expand All @@ -34,11 +34,13 @@ runs:

Click `+` in either region; the count moves in lockstep. The
`session="tour"` attribute groups the two embeds as authoring intent;
the actual state sharing is delivered by the handler's
`ctx.BroadcastAction("Increment", nil)` line above — that line tells
the runtime to apply `Increment` to every other connected client
when one client clicks. Without it, each region's session would have
its own count; with it, they stay synced.
the actual state sharing is delivered by the Mount-side
`ctx.Subscribe(ctx.SelfTopic())` opt-in plus the handler's
`ctx.Publish(ctx.SelfTopic(), "Increment", nil)` line above — that
pair tells the runtime to apply `Increment` on every other connected
client that subscribed when one client clicks. Without the Subscribe,
no peer registers as a receiver; without the Publish, no fan-out
happens; with both, the two regions stay synced.

```embed-lvt path="/apps/counter/" upstream="http://127.0.0.1:9090" session="tour"
```
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ require (
github.com/fsnotify/fsnotify v1.9.0
github.com/gorilla/websocket v1.5.3
github.com/lib/pq v1.10.9
github.com/livetemplate/livetemplate v0.9.0
github.com/livetemplate/livetemplate v0.10.0
github.com/livetemplate/lvt/components v0.0.0-20260228153051-c00a45caae95
github.com/rogpeppe/go-internal v1.14.1
github.com/stretchr/testify v1.11.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,8 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/livetemplate/livetemplate v0.9.0 h1:6vHNNpZLIY4gO08/Gth3vr1F3A3ANwSKTQvfuy1I8js=
github.com/livetemplate/livetemplate v0.9.0/go.mod h1:GMvZKyPUq8LSGfgD3pftKOHa6v+I+RDYyff2mNjeAYs=
github.com/livetemplate/livetemplate v0.10.0 h1:4b4H6FPm/b37m+3zcWEuTKJPMKSYFng6RGefIkT86XQ=
github.com/livetemplate/livetemplate v0.10.0/go.mod h1:GMvZKyPUq8LSGfgD3pftKOHa6v+I+RDYyff2mNjeAYs=
github.com/livetemplate/lvt/components v0.0.0-20260228153051-c00a45caae95 h1:ZeZBbM80f9J/hlVFaEJlnpzChGAy4nyMx3ziqXYg5RI=
github.com/livetemplate/lvt/components v0.0.0-20260228153051-c00a45caae95/go.mod h1:mR77vCY0w5oevC2Rz463tmcJosFG5ukDQLWronOHwEI=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
Expand Down
Loading