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
28 changes: 25 additions & 3 deletions .agents/skills/fusion-background-tasks/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
name: fusion-background-tasks
description: >-
Documents Fusion process-wide Tokio background tasks (spawn, spawn_after,
cancel, status) across Python, Node, and C#. Use when scheduling fire-and-forget
or delayed work off the request path.
cancel, status, snapshot) across Python, Node, and C#. Use when scheduling
fire-and-forget or delayed work off the request path, or when inspecting tasks
via the Fusion monitor panel.
---

# Fusion background tasks
Expand All @@ -19,10 +20,30 @@ Not a durable queue (no Redis/persistence).
| `tasks.spawn_after(ms, fn)` | `tasks.spawnAfter(ms, fn)` | `BackgroundTasks.SpawnAfter(ms, action)` |
| `tasks.cancel(id)` | `tasks.cancel(id)` | `BackgroundTasks.Cancel(id)` |
| `tasks.status(id)` | `tasks.status(id)` | `BackgroundTasks.Status(id)` |
| `tasks.snapshot()` | `tasks.snapshot()` | `BackgroundTasks.Snapshot()` |
| `tasks.reset()` | `tasks.reset()` | `BackgroundTasks.Reset()` |

Status values: `pending` | `running` | `done` | `cancelled` | `failed`.

`snapshot()` returns `{ task_count, active_count, tasks: [{ id, status, delay_ms, created_at_ms }] }`.
Terminal tasks are pruned (keep last 100) so the registry cannot grow forever.

### Pass a callable

```python
# Correct — defer the call:
tasks.spawn(lambda: test_task(name))

# Wrong — calls test_task immediately and passes None:
# tasks.spawn(test_task(name)) # TypeError: callback must be callable
```

## Fusion monitor

When `monitor.enabled` is true, the Fusion monitor HTML and `{path}/json` embed the
task list under `tasks` / a **Background tasks** card. Settings live under top-level
`monitor.*` (not under `cache.monitor`).

## Notes

- Callbacks may run on Tokio worker threads (Python holds the GIL only for the call).
Expand All @@ -31,7 +52,8 @@ Status values: `pending` | `running` | `done` | `cancelled` | `failed`.

## Examples

`examples/background_tasks.py` / `.mjs` / `.cs`
`examples/background_tasks.py` / `.mjs` / `.cs`
`examples/monitor.*` (spawns sample tasks for the panel)

## Implementation

Expand Down
5 changes: 3 additions & 2 deletions .agents/skills/fusion-bindings-parity/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,9 @@ New public surface → show usage in **Python + Node + C#**. Prefer the same bas
| Custom HTTP | `@http_get("path/[action]")` | `httpGet('path/[action]')(proto.method)` | `[HttpGet("path/[action]")]` |
| Middleware | `middleware.py` factories | factories in `index.js` | `Middleware.cs` |
| Static files | `static_files()` | `staticFiles()` | `Middleware.StaticFiles()` |
| Cache | `fusion_framework.cache` (moka) + monitor | `cache` export + monitor | `Cache` + `CacheMonitor` |
| Background tasks | `fusion_framework.tasks` (Tokio) | `tasks` export | `BackgroundTasks` |
| Cache | `fusion_framework.cache` (moka) | `cache` export | `Cache` |
| Fusion monitor | `monitor.mount_monitor` | `mountMonitor` | `FusionMonitor` |
| Background tasks | `fusion_framework.tasks` (+ snapshot; in monitor) | `tasks` export | `BackgroundTasks` |
| Permissions | `permissions=` / `require_permissions` | `permissions` / `requirePermissions` | `PermissionTypes` / `RequirePermissions` |
| OpenAPI / Swagger | `app.py` + `api_types.rs` | `buildOpenApi` in `index.js` | `Swagger.cs` |
| Version navbar | per-version OpenAPI routes | same | same |
Expand Down
52 changes: 25 additions & 27 deletions .agents/skills/fusion-cache/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ name: fusion-cache
description: >-
Documents Fusion application cache (default moka driver, Redis reserved),
settings under fusion.<env>.json, sync/async APIs, TTL rules, the optional
cache monitor panel, and clear across Python, Node, and C#. Use when adding
cache usage or changing drivers.
Fusion monitor panel (cache + tasks), and clear across Python, Node, and C#.
Use when adding cache usage or changing drivers.
---

# Fusion cache
Expand All @@ -24,17 +24,17 @@ Redis (`cache.driver = "redis"`) is reserved in settings but **not implemented y
"driver": "moka",
"max_capacity": 10000,
"default_ttl": null,
"max_events": 50,
"connection_string": null,
"host": "127.0.0.1",
"port": 6379,
"username": null,
"password": null,
"db": 0,
"monitor": {
"enabled": true,
"path": "/__fusion/cache",
"max_events": 50
}
"db": 0
},
"monitor": {
"enabled": true,
"path": "/__fusion/monitor"
}
```

Expand All @@ -43,12 +43,14 @@ Redis (`cache.driver = "redis"`) is reserved in settings but **not implemented y
| `driver` | `moka` (default) or future `redis` |
| `max_capacity` | moka max entries |
| `default_ttl` | seconds, or **`null` = no expiry** unless code passes `ttl=` |
| `max_events` | Ring-buffer size for recent set/delete/clear events (legacy: `cache.monitor.max_events`) |
| `connection_string` / `host` / `port` / `username` / `password` / `db` | Redis connection (future) |
| `monitor.enabled` | Mount HTML + JSON monitor routes (dev scaffold: `true`; stage/prod: `false`) |
| `monitor.path` | UI path; JSON at `{path}/json` (default `/__fusion/cache`) |
| `monitor.max_events` | Ring-buffer size for recent set/delete/clear events |
| `monitor.path` | UI path; JSON at `{path}/json` (default `/__fusion/monitor`) |

When `cache.monitor.enabled` is **false**, bindings must **not** register the monitor endpoints (security: disable routes, not only UI).
When `monitor.enabled` is **false**, bindings must **not** register the monitor endpoints (security: disable routes, not only UI). Legacy `cache.monitor.enabled` / `cache.monitor.path` still work.

The HTML panel and `{path}/json` include **cache entries**, **recent cache events**, and **background tasks**.

### TTL rules

Expand Down Expand Up @@ -86,35 +88,31 @@ Same rules apply to `get_or_set` / `delete_or_set` / `exists_or_set` and their a
| `await cache.aexists_or_set` | `await cache.aexistsOrSet` | `await Cache.ExistsOrSetAsync` |
| `await cache.aclear` | `await cache.aclear` | `await Cache.ClearAsync` |

Semantics:
Notes:

- **get_or_set** — return cached value, else store default (value or callable) and return it
- **aget_or_set** — same; factory may be **async**
- **delete_or_set** — delete then set; return stored value
- **exists_or_set** — if key exists return `true`; else set and return `false`
- **clear** — drop all keys (keeps the cache instance); **reset** (tests) drops the global instance
- **snapshot** — entries + recent events (monitor JSON)
- **panel_context** — template vars for `fusion/cache_monitor.html`
- **snapshot** — entries + recent events + embedded `tasks` object (monitor JSON)
- **panel_context** — template vars for `fusion/monitor.html` (including task table)

Values must be JSON-compatible.

## Cache monitor panel
## Fusion monitor panel

Built-in HTML panel (`FusionBaseTemplate` + `fusion.badge` / `fusion.table` / `fusion.card` / `fusion.button`) auto-mounted on `listen` / `Mount` when `cache.monitor.enabled` is true:
Built-in HTML panel auto-mounted on `listen` / `Mount` when `monitor.enabled` is true:

- `GET {path}` — HTML (auto-refresh every 5s)
- `GET {path}/json` — raw snapshot
- `GET {path}` — HTML (auto-refresh every 5s): cache entries, recent events, background tasks
- `GET {path}/json` — raw snapshot (includes top-level `tasks`)

Scaffold (fusion-tool): **dev** `enabled: true`; **stage/prod** `enabled: false`.

## Examples

`examples/cache.py` / `.mjs` / `.cs`
`examples/cache_monitor.py` / `.mjs` / `.cs`
`examples/monitor.py` / `.mjs` / `.cs`

## Implementation

- Core: `crates/fusion-core/src/cache.rs` (moka) + `assets/templates/fusion/cache_monitor.html`
- Python: `fusion_framework.cache` + `cache_monitor.mount_cache_monitor`
- Node: `cache` export + `mountCacheMonitor` in `FusionApp.mount`
- C#: `Cache` + `CacheMonitor.Mount` via FFI
- Core: `crates/fusion-core/src/cache.rs` + `monitor.rs` + `assets/templates/fusion/monitor.html`
- Python: `fusion_framework.cache` + `monitor.mount_monitor`
- Node: `cache` export + `mountMonitor` in `FusionApp.mount`
- C#: `Cache` + `FusionMonitor.Mount` via FFI
2 changes: 1 addition & 1 deletion .agents/skills/fusion-cli/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ C# (`asp-core`): `main.cs`, `*.csproj` (`net10.0`), `[Route]` / `[HttpGet]`.
- Convention verbs (`get` / `post` / …) plus one custom slot (`http_get` / `httpGet` / `[HttpGet]` with `[action]`).
- Opt-in middleware list in `main` (e.g. `request_id`, `cors`, `cache_headers`, `security_headers`, `framework_headers`). Framework does **not** auto-enable middleware; the scaffold opts in.
- Application cache defaults to **moka** (`cache` block in env JSON); see `fusion-cache` skill.
- Cache monitor (`cache.monitor.enabled`) is on in **dev** and off in **stage/prod**; when off, no monitor HTTP endpoints are registered.
- Cache monitor (`monitor.enabled`) is on in **dev** and off in **stage/prod**; when off, no monitor HTTP endpoints are registered.

### Default ports

Expand Down
13 changes: 13 additions & 0 deletions bindings/csharp/FusionFramework/BackgroundTasks.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Runtime.InteropServices;
using System.Text.Json.Nodes;

namespace FusionFramework;

Expand Down Expand Up @@ -69,6 +70,18 @@ public static bool Cancel(string taskId)
return Native.TakeUtf8(ptr);
}

/// <summary>JSON snapshot of tracked tasks (also under <see cref="Cache.Snapshot"/>).</summary>
public static JsonNode Snapshot()
{
var ptr = Native.fusion_task_snapshot();
if (ptr == IntPtr.Zero)
throw new InvalidOperationException("task snapshot failed");
var json = Native.TakeUtf8(ptr);
if (string.IsNullOrEmpty(json))
throw new InvalidOperationException("empty task snapshot");
return JsonNode.Parse(json) ?? new JsonObject();
}

/// <summary>Abort and clear all tracked tasks (tests).</summary>
public static void Reset() => Native.fusion_task_reset();
}
74 changes: 62 additions & 12 deletions bindings/csharp/FusionFramework/CacheMonitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,33 +4,65 @@
namespace FusionFramework;

/// <summary>
/// Built-in process-wide cache monitor (HTML + JSON).
/// Mounted only when <c>cache.monitor.enabled</c> is true — disabled means no endpoints.
/// Built-in Fusion monitor (cache + background tasks HTML + JSON).
/// Mounted when <c>monitor.enabled</c> is true (legacy <c>cache.monitor.enabled</c> still works).
/// </summary>
public static class CacheMonitor
public static class FusionMonitor
{
/// <summary>Register HTML and <c>/json</c> routes when enabled in settings.</summary>
public static bool Mount(FusionApp app, FusionSettings settings)
{
if (!Truthy(settings.Get("cache.monitor.enabled", false), false))
if (!Enabled(settings))
return false;

Cache.Configure(settings);
var path = NormalizePath(AsString(settings.Get("cache.monitor.path", "/__fusion/cache")));
var path = ResolvePath(settings);

app.AddRawRoute("GET", path, () => new CacheMonitorPanel().Get());
app.AddRawRoute("GET", path, () => new MonitorPanel().Get());
if (path != "/")
app.AddRawRoute("GET", $"{path}/", () => new CacheMonitorPanel().Get());
app.AddRawRoute("GET", $"{path}/", () => new MonitorPanel().Get());
app.AddRawRoute("GET", $"{path}/json", () => Cache.Snapshot());
return true;
}

/// <summary>Prefer <c>monitor.enabled</c>, else legacy <c>cache.monitor.enabled</c>.</summary>
static bool Enabled(FusionSettings settings)
{
var top = settings.Get("monitor.enabled", null);
if (top is not null && !IsJsonNull(top))
return Truthy(AsNode(top), false);
return Truthy(AsNode(settings.Get("cache.monitor.enabled", false)), false);
}

/// <summary>Prefer <c>monitor.path</c>, else legacy <c>cache.monitor.path</c>.</summary>
static string ResolvePath(FusionSettings settings)
{
var top = AsString(settings.Get("monitor.path", null));
if (!string.IsNullOrWhiteSpace(top))
return NormalizePath(top);
return NormalizePath(AsString(settings.Get("cache.monitor.path", "/__fusion/monitor")));
}

static bool IsJsonNull(object? value) =>
value is null
|| (value is JsonNode n && n.GetValueKind() == JsonValueKind.Null);

static JsonNode? AsNode(object? value) =>
value switch
{
null => null,
JsonNode n => n,
bool b => JsonValue.Create(b),
string s => JsonValue.Create(s),
_ => JsonValue.Create(value.ToString()),
};

static string NormalizePath(string? raw)
{
var path = string.IsNullOrWhiteSpace(raw) ? "/__fusion/cache" : raw.Trim();
var path = string.IsNullOrWhiteSpace(raw) ? "/__fusion/monitor" : raw.Trim();
if (!path.StartsWith('/')) path = "/" + path;
path = path.TrimEnd('/');
return string.IsNullOrEmpty(path) ? "/__fusion/cache" : path;
return string.IsNullOrEmpty(path) ? "/__fusion/monitor" : path;
}

static bool Truthy(JsonNode? value, bool defaultValue)
Expand Down Expand Up @@ -67,11 +99,19 @@ static bool Truthy(JsonNode? value, bool defaultValue)
}
}

/// <summary>Default cache monitor page (FusionBaseTemplate + built-in components).</summary>
public sealed class CacheMonitorPanel : FusionBaseTemplate
/// <summary>Backward-compatible alias for <see cref="FusionMonitor"/>.</summary>
public static class CacheMonitor
{
/// <inheritdoc cref="FusionMonitor.Mount"/>
public static bool Mount(FusionApp app, FusionSettings settings) =>
FusionMonitor.Mount(app, settings);
}

/// <summary>Default Fusion monitor page (cache + tasks).</summary>
public sealed class MonitorPanel : FusionBaseTemplate
{
/// <inheritdoc />
public override string TemplateName() => "fusion/cache_monitor.html";
public override string TemplateName() => "fusion/monitor.html";

/// <inheritdoc />
public override Dictionary<string, JsonNode?> Context()
Expand All @@ -86,3 +126,13 @@ public sealed class CacheMonitorPanel : FusionBaseTemplate
return dict;
}
}

/// <summary>Backward-compatible alias for <see cref="MonitorPanel"/>.</summary>
public sealed class CacheMonitorPanel : FusionBaseTemplate
{
/// <inheritdoc />
public override string TemplateName() => "fusion/monitor.html";

/// <inheritdoc />
public override Dictionary<string, JsonNode?> Context() => new MonitorPanel().Context();
}
2 changes: 1 addition & 1 deletion bindings/csharp/FusionFramework/FusionApp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ public void Mount()

Middleware.MountStaticFiles(this, _middleware);
SwaggerDocs.Mount(this, SettingsStore.Current);
CacheMonitor.Mount(this, SettingsStore.Current);
FusionMonitor.Mount(this, SettingsStore.Current);
}

internal void AddRawRoute(string method, string path, Func<object?> handler)
Expand Down
3 changes: 3 additions & 0 deletions bindings/csharp/FusionFramework/Native.cs
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,9 @@ public static extern IntPtr fusion_task_spawn_after(
[DllImport(LibName, CallingConvention = CallingConvention.Cdecl)]
public static extern void fusion_task_reset();

[DllImport(LibName, CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr fusion_task_snapshot();

[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate IntPtr FusionHandlerFn(
IntPtr userData,
Expand Down
1 change: 1 addition & 0 deletions bindings/csharp/FusionFramework/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ var id = BackgroundTasks.Spawn(() => Console.WriteLine("done"));
BackgroundTasks.SpawnAfter(1000, () => Console.WriteLine("later"));
BackgroundTasks.Cancel(id);
BackgroundTasks.Status(id); // pending|running|done|cancelled|failed
BackgroundTasks.Snapshot(); // also under Cache.Snapshot()["tasks"]
```
```

Expand Down
Loading
Loading