PulseTrade.Comm.Spa 0.2.4-beta6

This is a prerelease version of PulseTrade.Comm.Spa.
There is a newer prerelease version of this package available.
See the version list below for details.
dotnet add package PulseTrade.Comm.Spa --version 0.2.4-beta6
                    
NuGet\Install-Package PulseTrade.Comm.Spa -Version 0.2.4-beta6
                    
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.4-beta6" />
                    
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.4-beta6" />
                    
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.4-beta6
                    
#r "nuget: PulseTrade.Comm.Spa, 0.2.4-beta6"
                    
#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.4-beta6
                    
#: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.4-beta6&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=PulseTrade.Comm.Spa&version=0.2.4-beta6&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.4-beta6"

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.4-beta6"

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.4-beta6"

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 (1)

Showing the top 1 NuGet packages that depend on PulseTrade.Comm.Spa:

Package Downloads
PulseTrade.Comm.GW.PTCS

Package Description

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.2.4-beta7 17 6/21/2026
0.2.4-beta6 17 6/21/2026
0.2.3-beta5 39 6/20/2026
0.1.0-preview6 46 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