PulseTrade.Comm.Spa 0.2.3-beta5

This is a prerelease version of PulseTrade.Comm.Spa.
dotnet add package PulseTrade.Comm.Spa --version 0.2.3-beta5
                    
NuGet\Install-Package PulseTrade.Comm.Spa -Version 0.2.3-beta5
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="PulseTrade.Comm.Spa" Version="0.2.3-beta5" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="PulseTrade.Comm.Spa" Version="0.2.3-beta5" />
                    
Directory.Packages.props
<PackageReference Include="PulseTrade.Comm.Spa" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add PulseTrade.Comm.Spa --version 0.2.3-beta5
                    
#r "nuget: PulseTrade.Comm.Spa, 0.2.3-beta5"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package PulseTrade.Comm.Spa@0.2.3-beta5
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=PulseTrade.Comm.Spa&version=0.2.3-beta5&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=PulseTrade.Comm.Spa&version=0.2.3-beta5&prerelease
                    
Install as a Cake Tool

PulseTrade.Comm.Spa

英文版:README.en-us.md

PulseTrade.Comm.Spa 是從既有 PT.Comm web surface 萃取出的 Suave + WebSharper SPA POC package。目標是讓 FSI / NuGet #r 可以直接起一個最小通訊與資料互動框架,並保留 GitHub OAuth 與原本核心頁面。

縮寫與命名

名稱 正式專案 / package 建議縮寫 說明
PulseTrade.Comm PT.Comm / PulseTrade.Comm PT.Comm / PTC MCP gateway、agent/user comm tools、upstream forwarding、PTFR tool surface 與部署邊界。PTCPT.Comm 可接受且建議使用的短縮寫;避免再寫成 PTC.Comm
PulseTrade.Comm.Spa PT.Comm.Spa / PulseTrade.Comm.Spa PTCS / spa 本 package,負責 /chat/sets/actors UI semantics、Suave/WebSharper browser surface 與目前 package-provided ActorFabric
PulseTrade.fs.realworld PT.fs.realworld / PulseTrade.fs.realworld PTFR 真實世界交易研究 sandbox ledger;不是 comm gateway,也不是 SPA UI。

與 PT.Comm / PTC 的整合規劃

短期規劃:

  1. PT.Comm 直接透過 NuGet 參考本 package,不複製本 repo source,也不把 SPA UI route 搬回 PT.Comm 自己實作。
  2. 同一個 PT.Comm process 可以同時啟動 ASP.NET Core/Kestrel 的 MCP/GW listener,以及 Suave/WebSharper 的 SPA listener;兩者使用不同 port,不應互相搶 route。
  3. /chat/sets/actors 的 UI semantics 由本 package 擁有;/mcp/.well-known/*、gateway diagnostics、PTFR/GitHub/FSharpDevKit forwarding 由 PT.Comm 擁有。
  4. comm participant/message/thread/inbox 的 canonical runtime 應收斂到本 package 目前提供的 ActorFabric / CommHub,讓 MCP comm_* tools 與 SPA UI 操作同一套通訊 fabric。

中長期規劃:

  1. ActorFabric 是 communication runtime concern,不是 UI concern;長期應從本 package 抽成獨立 fabric package,例如 PT.Comm.Fabric
  2. 抽出後,本 package 只保留 browser UI、WebSharper/Suave surface、browser sync、human OAuth 與 read model presentation。
  3. 在 fabric package 尚未抽出前,文件用語應寫作「PTCS package-provided fabric」,避免誤解為「SPA UI owns fabric」。

常見 FSI 與瀏覽器疑問請先看:Q&A.md。完整 RFC / SA / SD / WBS / Test / Runbook 文件鏈入口:doc/Traceability.md

Package 分層與最小設計

這個 package 的 default path 必須保持最小:FSI user 應該能用少量 F# 啟動 /chat/sets/actors,再依需要 opt-in 進階能力。

Core
  command envelope / stream key / SeqId / append page contracts / browser shell
Default local profile
  in-memory Akka Journal / AutoLocal actor fabric / OAuth disabled / random port
Optional profiles
  GitHub OAuth / SQL Server journal / PCSL writer node / cluster binding
Examples
  Scripts/Demo/* / README / roadshow seed data
Tests
  Playwright / PCSL repair / multi-instance / SQL integration

設計約束:

  • Core 不要求 SQL Server、GitHub OAuth、固定 port、Fortigate、PCSL root、multi-node cluster 或 demo seed data。
  • CommHub / HTTP / FSI helper 是人類好讀 facade;會改變狀態的操作應收斂到 command gateway。
  • PCSL 是 projection/cache,不是 canonical truth;canonical reality 是 Akka Journal。local POC 預設可以用 in-memory journal。
  • GitHub OAuth、SQL Server journal、PCSL writer node、cluster/multi-instance 都是 explicit opt-in profile。
  • Demo scripts 只屬 examples;demo-only helper 與 seed data 不進 runtime core。

目前 package boundary audit 見:doc/PackageBoundaryAudit.md

保留路由

  • /chat
  • /sets
  • /actors
  • 透過 CommHub.RegisterAppendPage、Web page creator 或 sharded append-page intent 動態註冊的 append pages,例如 /fcell-chat/fcell-list/fcell-grid、actor-address Argu fCell chat pages

OAuth

  • GitHub browser OAuth 保留 /chat/login/chat/oauth/callback/chat/logout
  • OAuth secret 只用檔案路徑傳入,例如 --client-secret-pathClientSecretPath;不要把 secret value 寫進 script、文件、log 或 repo。

從原始碼啟動

dotnet run --project .\PulseTrade.Comm.Spa.fsproj -c Release -- `
  --port 8897 `
  --pcsl-root .\.pcsl\run

FSI 基本形狀

#r "nuget: PulseTrade.Comm.Spa, 0.2.3-beta5"

open PulseTrade.Comm.Spa

let minimalApp = Server.startMinimal()
printfn "%s/chat" minimalApp.Url

// 需要指定 PCSL root 時,使用 seed-free hub + random local port。
let hub = CommHub.createEmptyWithPcslRoot @".\.pcsl\fsi"

let pcslApp =
  hub
  |> ServerOptions.localRandomWithHub
  |> Server.start

printfn "%s/chat" pcslApp.Url

pcslApp.Hub.RegisterParticipant
  { ParticipantId = "agent.demo"
    DisplayName = Some "Demo Agent"
    Kind = Some "agent"
    Labels = Some [ "demo" ] }
|> ignore

pcslApp.Hub.AppendSet
  { Keys = [ "user.github.alice"; "agent.demo" ]
    SetName = "chat"
    Value = "hello"
    Tags = Some [ "fsi" ] }
|> ignore

// minimalApp.Dispose() / pcslApp.Dispose() 會停止 Suave。

Persistent Journal / Replay

預設 profile 使用 in-memory Akka Journal,適合 FSI POC 與單次 demo。需要 persistent journal 時,使用 journal profile 明確 opt-in;PCSL 仍是 projection/cache,actor recovery 會從 Akka Journal replay 後補回 fresh PCSL backend。Scripts/verify.sqlJournalReplay.fsxScripts/Demo/09-persistent-journal-replay.fsx 會用 SQL Server journal 實際寫入,然後換 fresh PCSL root 由 journal replay 補回 page/key/value projection。

#r "nuget: PulseTrade.Comm.Spa, 0.2.3-beta5"

open PulseTrade.Comm.Spa

let journal =
  Journal.sqlServerLocal(dbName = "PulseTradeCommSpa")

// 只建立 database;journal/snapshot tables 由 Akka.Persistence.Sql 啟動時 auto-initialize。
let bootstrap = Journal.ensureSqlServerDatabase journal
printfn "journal db ready: %s created=%b" bootstrap.DatabaseName bootstrap.Created

let runtime = Journal.checkRuntimeHealth journal
printfn "journal open=%A query=%A error=%A" runtime.ConnectionOpenSucceeded runtime.QuerySucceeded runtime.LastError

let queryAdapter = Journal.sqlServerQueryHealthAdapter()

let fabricOptions =
  CommSpaActorFabricOptions.defaults
  |> CommSpaActorFabricOptions.withJournal journal
  |> CommSpaActorFabricOptions.withJournalQueryAdapter queryAdapter

let persistentApp =
  ServerOptions.localRandom()
  |> ServerOptions.withActorFabricOptions fabricOptions
  |> Server.start

printfn "%s/chat" persistentApp.Url

// Site 啟動後也可查詢:
//   GET <app.Url>/healthz          // cheap provider metadata
//   GET <app.Url>/healthz.journal  // explicit runtime + journal/projection reality probe

Durable ingress retry / dead-letter policy

DurableIngress 的 retry/dead-letter policy 是 runtime profile 設定,不代表 browser pending retry,也不宣稱 exactly-once。分層語意:

  • ResendIntervalMin / ResendIntervalMax:delivery protocol 的 resend 節奏。
  • CommandMaxAge:ingress 邊界可接受的 command 年齡。
  • CommandDeadline / DeadlineAtUtc:command 到期後應 reject 或進 failed/dead-letter task status。
  • PoisonAttemptHint:提供 operator/diagnostics 的 poison threshold hint,不是唯一的重送次數保證。
  • DeadLetterStreamKey:後續 durable profile 投影 failed/dead-letter 的 stream key。
let retry =
  { DurableDeliveryRetryOptions.defaults with
      ResendIntervalMin = TimeSpan.FromMilliseconds 250.0
      ResendIntervalMax = TimeSpan.FromSeconds 3.0
      CommandMaxAge = Some(TimeSpan.FromMinutes 5.0)
      CommandDeadline = Some(TimeSpan.FromSeconds 30.0)
      PoisonAttemptHint = Some 7
      DeadLetterStreamKey = "ptcs.dead-letter" }

let ingressOptions =
  { CommSpaDurableIngressOptions.volatileLocal() with
      Mode = DurableIngressMode.DurableDelivery
      ProfileId = "local-durable-policy"
      Retry = retry }
  |> CommSpaDurableIngressOptions.normalize

目前 CommSpaDurableIngress.createVolatile 會 expose normalized retry options,並在 ingress 邊界拒絕 expired CommandMaxAge / DeadlineAtUtc / CommandDeadlineSpawnAsync 建立的 task ticket 可由 CompleteAsync / FailAsync 轉成 completed/failed 狀態;ActorArgu.sendDurableAsync 已使用這個 boundary,讓 actor-address raw argu command 先 accepted,再依 reply/error 完成或失敗 ticket。

CommSpaMessageFabric.createDurable hub ingress 會建立 durable wrapper:RegisterParticipant、UpsertGroup、Send、Ack、Drain 先 accepted ticket,再執行既有 MessageFabric projection,成功後 CompleteAsync。Poll/Wait/List 仍讀 projection/fast path。既有 CommSpaMessageFabric.create hub 不變,適合 local/POC 或不需要 durable admission 的 caller。

真正 Akka.Delivery / Sharding.Delivery producer queue、restart retry 與 provider-specific dead-letter projection 仍屬後續 durable profile 切片。

RFC-0005 Dynamic PCSL / Task Result / Agent Task POC

RFC-SPA-UPSTREAM-0005 的 first-slice package consumer gate 是:

dotnet fsi --exec .\Scripts\verify.rfc0005PackageConsumer.fsx

這支腳本使用 defaultArgumentsText -> ParseLine -> Argu,可在 Visual Studio FSI 直接修改字串參數後框選執行。它覆蓋:

  • journal namespace / persistence id prefix / projection id / projection epoch;
  • same-journal projection rebuild plan;
  • journal merge dry-run first-slice policy;
  • volatile task result vault retention / max result bytes;
  • CommSpaDurableMessageFabric.SubmitAgentTaskDurableAsync agent-task handoff;
  • MessageFabricGatewayConsumerContract 的 PTC.GW / PTCS ownership boundary。

它不啟動 web server、不做 OAuth、不宣稱 crash-durable result vault、runtime hot switch 或 PTC.GW 專案端已完成整合。

固定或隨機 Port

let minimalApp = Server.startMinimal()
printfn "%s/chat" minimalApp.Url

let randomOptions = ServerOptions.localRandom()

let randomApp = Server.start randomOptions
printfn "%s/chat" randomApp.Url

let fixedOptions =
  ServerOptions.defaults
  |> ServerOptions.withWebBinding (WebBinding.fixedPort 81)

let fixedApp = Server.start fixedOptions
printfn "%s/chat" fixedApp.Url

CQRS Snapshot 範例

let streamKey =
  CommSpaStreamKey.forSet "chat" [ "agent.demo"; "user.github.alice" ]

let snapshot =
  app.Hub.StreamSnapshot
    { StreamKey = streamKey
      DesiredTailCount = 200
      BrowserWatermark = None
      IncludeMetadata = true }

snapshot.MissingTailEvents
|> List.iter (fun event -> printfn "%d %s" event.Sequence event.Payload)

目前 HTTP restart/reconnect POC surface:

  • POST /sync/api/snapshot
  • request body:SnapshotRequest
  • response body:SnapshotReply

Shared Site 模式

同一個 process 裡,多個 library 可以用相同 normalized options 共用同一個站台:

let sharedHub = CommHub.createWithPcslRoot @".\.pcsl\shared"

let options =
  { Host = "0.0.0.0"
    Port = 81
    Hub = sharedHub
    OAuth = OAuth.disabled
    ActorFabric = AutoLocal }

let appFromA = Server.startShared options
let appFromB = Server.startShared options

obj.ReferenceEquals(appFromA.Hub, appFromB.Hub) // true

appFromA.Stop()
appFromB.Stop()

fCell2 Key/Value 形狀

相關 source project:

  • G:\coldfar_py\sharftrade9\Libs5\KServer\FCell2\FAkka.FCell2.fsproj
  • G:\coldfar_py\sharftrade9\Libs5\KServer\FCell2.WebSharper\FAkka.FCell2.WebSharper.fsproj

FSI / server 端使用 canonical fCell2<string>。Browser-side WebSharper 透過 FAkka.FCell2.WebSharper 共用 vocabulary;該 package 定義 JS,所以 D 在 JS 端編譯為 float,非 JS 端則保留 decimal。

Web 端可以直接在 /chat 頁面上方建立 fCell 系列頁面,不需要先在 FSI seed:

  • Shape 選 FCell ChatFCell ListFCell GridActor Argu
  • 填入 page id / title 後按 Add;
  • 進入新 tab 後,左側用 Add 建立 key;
  • chat/list/grid 的 append 會寫到目前 key;
  • Actor Argu 的 key 是 actor address,輸入框內容是 Argu-style string,server 會 route 到該 actor,reply 會回到同 key 的 fCell chat history。
#r "nuget: PulseTrade.Comm.Spa, 0.2.3-beta5"

open PulseTrade.Comm.Spa
open PersistedConcurrentSortedList.Type
open FAkka.FCell2

let fcellHub = CommHub.createWithPcslRoot @".\.pcsl\fcell"

let app =
  Server.start
    { Host = "127.0.0.1"
      Port = 8897
      Hub = fcellHub
      OAuth = OAuth.disabled
      ActorFabric = AutoLocal }

app.Hub.RegisterAppendPage(AppendPage.fCellChat "fcell-chat" "FCell Chat" "fcell chat") |> ignore
app.Hub.RegisterAppendPage(AppendPage.fCellList "fcell-list" "FCell List" "fcell list") |> ignore
app.Hub.RegisterAppendPage(AppendPage.fCellGrid "fcell-grid" "FCell Grid" "fcell grid") |> ignore

let s text = fCell2<string>.S text
let a values = fCell2<string>.A values
let t fields = fCell2<string>.T(Map.ofList fields)

app.Hub.AppendPageValue
  { PageId = "fcell-chat"
    Keys = [ s "Aster" ]
    Value = s "orz"
    Direction = Some "inbound-message"
    Tags = Some [ "fsi" ] }
|> ignore

app.Hub.AppendPageValue
  { PageId = "fcell-list"
    Keys = [ s "Aster" ]
    Value = a [| s "orz2"; s "orz3" |]
    Direction = None
    Tags = Some [ "fsi" ] }
|> ignore

app.Hub.AppendPageValue
  { PageId = "fcell-grid"
    Keys = [ s "Aster" ]
    Value =
      a [| t [ "column3", s "orz"; "column1", s "orz3" ]
            t [ "column1", s "orz"; "column2", s "orz3" ] |]
    Direction = None
    Tags = Some [ "fsi" ] }
|> ignore

Browser-side WebSharper 形狀

open PersistedConcurrentSortedList.Type
open FAkka.FCell2

let tab = fCell2<string>.S "tab.chat"

let value =
  fCell2<string>.T(
    Map.ofList
      [ "body", fCell2<string>.S "hello from browser"
        "score", fCell2<string>.D 12.34 ])

let keyText = FCell2Text.key tab
let valueText = value.toJsonString()
Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.2.3-beta5 0 6/20/2026
0.1.0-preview6 3 6/20/2026
0.1.0-preview4 49 6/5/2026
0.1.0-preview3 47 6/5/2026
0.1.0-preview2 45 6/5/2026
0.1.0-preview1 53 6/4/2026