PulseTrade.Comm.Spa 0.2.25

There is a newer version of this package available.
See the version list below for details.
dotnet add package PulseTrade.Comm.Spa --version 0.2.25
                    
NuGet\Install-Package PulseTrade.Comm.Spa -Version 0.2.25
                    
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.25" />
                    
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.25" />
                    
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.25
                    
#r "nuget: PulseTrade.Comm.Spa, 0.2.25"
                    
#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.25
                    
#: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.25
                    
Install as a Cake Addin
#tool nuget:?package=PulseTrade.Comm.Spa&version=0.2.25
                    
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 與原本核心頁面。

開發者或 agent 在修改 PTCS / PTCS Host / Dynamic UI 前,必須先讀 doc/SDK_UI_Orientation.md。PTCS 是 Chat / MessageFabric / ActorFabric / Actors topology / Dynamic FormInput / Canvas 的互動型 SPA,不是 generic host status landing page;/healthz、OpenAPI、Swagger 只能是 diagnostics,不是登入後主畫面。

Current Package State

  • Current package slice: PulseTrade.Comm.Spa 0.2.25, paired with independently versioned PTCS.Dynamic packages for Dynamic/ACL/Login and TA surfaces. It includes the fixed Management page, durable page/participant lifecycle, read-only View As, bounded browser presence/Actor Registry projection cost, page/set/participant current-page selection and batch operations, bounded generic-set metadata projection, physical Management set/page purge, and legacy Delete tombstone reconciliation. 0.2.24 was superseded before deployment because its same-millisecond delete phase idempotency key could omit the completed marker. Formal Host rollout follows the exact-package deployment gate on public 81/local-login 82/internal ingress 8798.
  • Formal host deployment must use exact package references. Public GitHub OAuth listens on 81, SQL local login on 82, and durable GW ingress on 8798; the deployed artifact path and live gate are maintained in doc/Verification.md.
  • PTCS.ACL / PTCS.Login runtime slice is active: Server.withAcl, Server.withPtcsLogin, shared BrowserAuthProvider, pure WebSharper /login, HttpOnly session cookie, same-origin write gate, /acl/api/snapshot, /acl/api/manifest, protected POST /acl/api/reload, /sync/ws per-frame ACL proxy gate with per-frame principal re-resolve, per-connection proxy cleanup, JSONL/SQL ACL audit sink/readback, HTTP-normalized page id to ACL canonical resource mapping, reusable HTTP allow/deny/challenge/cross-origin/reload matrix, and client-side capability hiding are available. Remaining production gaps are public 81 SQL audit switch after service identity/DB permission decision and optional visual pixel-diff automation.
  • C:\Users\Administrator\test_gemini\PulseTrade.Comm.Spa.Dynamic\src\poc.full.nuget.journal.ACL.fsx starts two PTCS hosts over one hub/fabric: port 81 for GitHub OAuth and port 82 for local PTCS.Login. Non-conflict verification used 18081/18082 and passed no-wait plus Playwright MCP browser checks.
  • Actor Registry projection now treats lifecycle events as sequence-ordered facts. Actor registry stream SourceId is lifecycle-specific, so Registered and Unregistered for the same actor are not collapsed by idempotency.
  • Default /actors active views filter terminated/stopped actor rows; includeOffline=1 keeps those rows for diagnostics. This fixes the PingPong reload case where a stopped actor still appeared in the Dynamic ActorTree DSL.
  • RegisterActor / ProjectActorRegistryEvent remain command-first: they append stream events and read projected state, rather than directly mutating live CommHub state.

Durable Chat 與已讀游標

  • 正式 host 的 --pcsl-root 決定 chat、MessageFabric inbox 與 ack cursor 的 projection 位置;目前正式部署使用 G:\PulseTrade.fs.Comm.SpaPcsl
  • 對話、收件匣與已讀位置分別持久化於 set=chatmessage-fabric-inboxmessage-fabric-ack streams。GW task store/result vault 是其他資料域,不能用來判斷 chat 是否遺失。
  • /chat 初次載入、F5 與 Reload 一律向 backend 取最新 200 筆作 authoritative tail;IndexedDB 只供立即呈現。後續 polling使用after cursor增量讀取;使用者捲到thread頂端時才以durable sequence載入前一批200筆,並保留原閱讀位置。
  • CommSpaMessageFabric.ReadInboxAsync(participantId, maxMessages) 會讀 persisted ack cursor,只回後續訊息,非空批次才將 cursor 推進到該批最後一筆 MessageIdPoll 保持 read-only。
  • public MCP comm_inbox_ack 必須提供非空 cursor。package-level AckAsync(..., Cursor = None) 僅保留給明確的內部 reset/maintenance caller,不應由 public tool 暴露。
  • 具名 participant 的 PollInboxAsyncReadInboxAsyncWaitInboxAsyncAckAsync 會以五分鐘 bounded heartbeat 續租 presence。這不改變 message/ack read semantics,也不會讓每次 poll 寫 event;底層 registration bucket仍會跨 fabric instance抑制同一時窗的重複寫入。持續讀取的participant保持online,停止活動滿一小時後轉為offline。
  • Chat thread只在 append 前已位於底部且本批實際新增訊息時跟隨到底部。人工上捲後,空 poll或新訊息都不改變閱讀位置;回到底部後才恢復follow。
  • 左側標題列的 Export會將目前已載入、目前選取participant的thread輸出為JSONL;每列包含messageIdspeakercreatedAtUtcbody。切換participant會清空export selection,避免跨聊天室混入資料。

Management 與 View As

  • /management 是固定 system page,不屬於 append-page registry。頁面、set contents與participant清單皆由WebSharper ListModel驅動,預設每頁10筆,可切換10/20/40/all;三個grid都有本頁全選與跨頁selection,pages/participants可批次Show/Hide/Delete,sets可批次Delete。
  • 一般 tab 上的 x 只寫入 hide event;它讓頁面離開導覽列,但不刪除 lineage。Management 的 Show 可恢復。
  • Management 的 page Delete 寫入 durable lifecycle marker,並實體清除該lineage的value與page-key streams。相同 PageId + TabId 在 host 重啟與 startup registration 後仍不會復活;要重建同名頁面必須使用新的 TabId。刪除visible page成功後會立即同步definitions、browser cache與nav,不留下Not Found tab。
  • Pages/Set contents grid的header checkbox只選取或取消目前分頁,保留其他分頁既有selection;Pages可對已選lineage批次Show/Hide,完成後以authoritative rows與definitions同步grid/cache/nav。
  • Management 的 set Delete 以SetName + normalized Keys先寫registry tombstone,再實體清除該value stream的PCSL .val/.index chunks與backend cache;registry只保留最小control event。後續新value建立新lineage。Management與/sets?metadataOnly=true只對一般active stream讀tail-1,不再逐stream讀最多1,000筆values。
  • Participant Hide 只影響 roster visibility,不刪sets。Participant Delete會unregister identity、隱藏刪除時間以前該participant作為recipient的非公頻訊息,並physical purge keys中精確包含該participant id的active set streams;substring或node:<id>不匹配。public與該participant已送出的其他訊息不被抹除,重新註冊後的新inbound與新set values可讀。
  • CommHub.PurgeLegacyManagementTombstones()是0.2.25升級migration seam。它清理舊版Delete tombstone遮蔽且未復活的participant/set/page payload/key streams;新版Hide帶disposition:reversible-hide,不會被migration清除。共享registry仍保留不含payload的最小audit/control marker。
  • /sets使用sets-state-v2避免載入舊的大型browser snapshot;sidebar一次最多render 200個bucket,超出時以filter定位,其WebSocket tail先整批merge再單次render/cache write。
  • View as 位於 Logout 左側。它使用 server-issued opaque、HttpOnly、SameSite=Strict 的短期 cookie,區分 actual principal 與 effective viewed participant;只允許瀏覽對方非公頻 thread,不允許 HTTP 或 WebSocket send。server restart、grant expiry、target delete 或 cookie 竄改都會回到本人視角。
  • Chat roster 的canonical API為/chat/api/participants/chat/api/agents僅保留相容別名。Roster固定包含public,並列出未retired的user與agent peers、排除目前effective participant。View As agent.aster時,實際登入的user.github.*仍是可選peer。Roster每30秒更新;roster/thread read polling對同一Hub + actual participant最多每五分鐘寫一次presence,首次載入與send仍即時寫入。活動期間顯示online,停止活動一小時後才轉offline。
  • Management read、page/participant hide/show/delete 與 View As 都有獨立 ACL action;ACL disabled 時維持 legacy 相容行為,但 View As 仍強制唯讀。

縮寫與命名

名稱 正式專案 / 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。UI/SDK 心智模型先看:doc/SDK_UI_Orientation.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
  • /management
  • 透過 CommHub.RegisterAppendPage、Web page creator 或 sharded append-page intent 動態註冊的 append pages,例如 /fcell-chat/fcell-list/fcell-grid、actor-address Argu fCell chat pages

Actor Argu / Actor Dynamic

Actor ArguActor Dynamic 共用 PTCS 的 actor-argu command path,但 user-facing 能力不同:

Page type Add actor key Add target key Add proxy key Reply rendering
Actor Argu Add Actor Key由platform provider轉接native actor;不要求PTCS DTO Proxy actor + target actor + DU/template + canonical arg string FormInput 不支援 一般 fCell chat
Actor Dynamic 單一 actor address,輸入任意字串/JSON DU/template + canonical arg string FormInput Dynamic-owned live proxy key schema=fskynet-sdui 才 canvas render,否則一般呈現

PTCS core 不解析 Dynamic key 後續 segment,也不 reference PTCS.Dynamic 或 PTC RN package。Core 只保存 ordered AppendPageKey.Keys、提供 action shell、pending replay、actor-argu route 與 fallback UI。Dynamic package 擁有 FormInput/canvas/proxy key semantics。

ActorArguTargetCommand 會攜帶 ActorAddressTargetActorAddress optionRawArgu,但它是PTCS provider/explicit proxy contract,不是developer-facing native actor contract。Direct Add Actor Key走PlatformManagedNative;預設local provider將raw text包成fCell2<string>並期待fCell2<string> reply。Production Host必須安裝具備stable adapter、crash durability、retry/confirm與terminal completion proof的provider。

當selected key是[proxyActorAddress; "target-v1"; targetActorAddress; duTypeOrTemplateKey; canonicalArgString]時,route kind是ExplicitProxyTarget,第三段投影成TargetActorAddress = Some targetActorAddress並把typed command送到明確proxy。UI不透過hook偷換persisted key head。

Host可在fabric啟動後安裝provider:

let proof = fabric.UseActorArguDispatchProvider provider
ActorArguDispatchProvider.productionMissingRequirements provider

HTTP、WebSocket、ActorArgu.sendAsync fabricActorArgu.sendDurableAsync共用該provider slot。ActorArgu.sendWithRuntimeDetailedAsync只保留給明確需要legacy typed-direct的低階caller。

需要在terminal reply寫入host-owned control state時,使用CommSpaActorFabric.UseActorArguTerminalObserver。Observer收到normalized PageActorArguDispatchRequestActorArguTargetReply;執行點位於provider terminal success之後、成功history append之前。Observer未安裝時不改變既有行為;observer拋錯時不重新dispatch,也不append成功reply,而是走既有controlled error history。若host已有observer,新增consumer應先保存並chain既有observer,不可無聲覆蓋。

/actors page 的 tree/table/card 會優先呈現 backend-provided full actor address,例如 akka.tcp://system@host:port/user/name。若 projection 只有 local path,UI 會顯示現有 path,不自行捏造 host/port。

RFC-PTC-SPA-0010.actors-page-dynamic-dsl-rendering.md 將 beta38 的 Canvas summary card 視為過渡 proof。Current design 是 Actors page 專屬 rendering path:/actors 會把同一份 ActorTreeDocument 轉成 schema=fskynet-sdui / surface=ActorsPage / documentType=ActorTopologyPage 的 page-level DSL,先交給 PulseTradeRegisterPageRenderer 註冊的 Dynamic page renderer。

有 Dynamic extension 且 renderer 回 Some node 時,整個 /actors page host 由 Dynamic renderer 接管,PTCS core 不同時 mount fallback tree/table。沒有 Dynamic extension、沒有 ActorsPage renderer、renderer 回 None 或 throw 時,PTCS core 直接使用現有 fallback tree/grid/table,避免 Actors 頁面空白。

2026-06-28 first gates 使用 Scripts/run.actorsPageDynamic.localHost.fsx 啟動 source-based PTCS host,並載入 PTCS.Dynamic source Release bundle 驗證:PulseTrade.PageRenderers / PulseTrade.MessageRenderers 都可接到 ActorTopologyPage,Dynamic accepted 時 /actors 只 mount data-testid="dynamic-actors-page",fallback rows 為 0。後續 beta39 / Dynamic beta24 package rollout 已部署到 public 81 live81-ptcs-beta39-dynamic-beta24-hierarchy-restore-202606282340,確認 /actors 呈現 Actors / Dynamic 而不是 beta38 Canvas-summary/fallback split view,且 boxed + / - collapse、status dot、connector line 與 /user//system virtual ancestors 都可見。2026-06-29 beta40 / Dynamic beta27 進一步修正 Dynamic accepted ownership:renderer 回 Some node 時,PTCS core 會隱藏 fallback nodes container,且不再追加 data-testid="actor-node" / actor-card core cards;2026-06-29 Dynamic beta29 追加 browser-local report schedule start/stop,並以更嚴格 bundle verifier 排除 beta28 stale schedule JS。2026-06-29 beta41 / Dynamic beta30 新增 backend offline normalization:Reload 預設只渲染仍在線的 actor-system nodes,stale/offline node 只留給 includeOffline=true 診斷路徑。Current public 81 release 是 live81-ptcs-beta41-dynamic-beta30-offline-poc2-202606290748,已由 Playwright MCP 驗證,證據是 G:\PulseTrade.fs\log\20260629\public81-actors-beta41-dyn30.pngG:\PulseTrade.fs\log\20260629\public81-actors-beta41-dyn30-snapshot.mdG:\PulseTrade.fs\log\20260629\public81-actors-beta41-dyn30-dom.jsonScripts/verify.actorsPageDynamic.playwright.fsx 目前提供 reusable F# Playwright accepted-path gate,並檢查 core fallback cards 為 0、report schedule start/stop、以及 deterministic online PTCS/GW/RN fixture probe listeners。2026-06-29 Scripts/verify.actorsActorTree.playwright.fsx -- --with-unsupported-client-extension 覆蓋 extension manifest 存在但沒有可用 ActorsPage renderer 的 fallback path;Playwright MCP evidence:G:\PulseTrade.fs\log\20260629\20260629001159.actors-unsupported-fallback-playwright-mcp.png。剩餘 WBS 是 server-side persisted report schedule、IndexedDB/restart sync、GW/RN registry feed、includeOffline diagnostics UI 與 sharding failover/passivation visual state。

Client Extension Transient Channel

PTCS beta81 提供 generic extension-transient WebSocket protocol,讓 Dynamic/TA 等 client extension 在既有 same-origin /sync/ws 上交換非歷史性的 snapshot/action/poll frame。PTCS core只負責session identity、ACL、bounded lifecycle、dedupe與opaque payload,不解析extension schema,也不把frame寫入append journal、MessageFabric、PCSL page stream或IndexedDB history。

Server註冊:

let handler: ClientExtensionTransientHandler =
  fun context ->
    async {
      // context.Session 是server-derived;不要相信payload內自稱的user/session。
      return Ok ("opaque reply for " + context.Operation)
    }

hub.RegisterClientExtensionTransientHandler("ta-research", handler) |> ignore

使用Server.start時,server會把同一CommHub registry接到existing/dedicated WebSocket handlers。若自行持有fabric,也可明確替換resolver:

fabric.UseClientExtensionTransientHandlerResolver hub.TryFindClientExtensionTransientHandler

Browser adapter不放在PTCS core bundle。Beta81曾把broker直接加入Client.fs,但下游WebSharper Bundle在dependency merge時會讓wsfsc.exe無診斷崩潰;beta82移除該client surface,保留server protocol;beta85已由獨立PTCS.Dynamic adapter的真正CommHub + CommSpaActorFabric + Server.start + /sync/ws browser gate驗證。禁止用raw JavaScript或HTTP polling補洞。

有ACL時需explicit allow action ptcs.extension.transient,resource kind=ptcs.extension、resource id=extensionId。每session最多8 channels、全handler 1024、每channel 256 recent request ids、payload上限2 MiB、handler timeout 15秒;重連後是新session,adapter需重新open/resync。

Host 若需要依 command payload 做更細的 domain ACL,可在既有 route/frame gate 後安裝 authenticated command-target resolver。Resolver 只負責把 opaque command 映射到既有 action/resource,不執行 business handler;principal 一律取自 server session。未安裝時維持舊行為,resolver error/exception 則 fail closed:

let aclOptions =
    aclOptions
    |> PtcsAcl.withCommandAuthorizationTargetResolver resolver

Open facade package PulseTrade.Comm.Spa.ACL 0.1.0-alpha22 提供相同 API;完整 contract 與執行順序見 doc/ACL/SD.PTCS.ACL.md

驗證:Scripts/verify.clientExtensionTransient.fsx

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.5-beta117"

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.5-beta116"

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

Journal / Snapshot / DB tables

Package default is Journal.inMemory(), so no SQL DB is used unless the caller explicitly selects Journal.sqlServer or Journal.sqlServerLocal.

Current SQL profile defaults:

  • Journal.sqlServerLocal(?dbName=...) default DB: PulseTradeCommSpa.
  • Verification scripts often use PulseTradeCommSpaVerify, PulseTradeCommSpaQueryVerify_<suffix>, or explicit --sql-db values.
  • The deployed PTCS host only uses SQL journal when its host options select the SQL profile; otherwise it stays on in-memory journal plus PCSL projection/cache.

PTCS does not maintain custom CREATE TABLE scripts for Akka journal/snapshot. It delegates table creation to Akka.Persistence.Sql 1.5.67 with auto-initialize = true.

Effective PTCS SQL profile:

akka.persistence {
  journal {
    plugin = "akka.persistence.journal.sql"
    sql {
      class = "Akka.Persistence.Sql.Journal.SqlWriteJournal, Akka.Persistence.Sql"
      connection-string = "<redacted>"
      provider-name = "SqlServer2019"
    }
  }
  query.journal.sql {
    class = "Akka.Persistence.Sql.Query.SqlReadJournalProvider, Akka.Persistence.Sql"
    connection-string = "<redacted>"
    provider-name = "SqlServer2019"
  }
  snapshot-store {
    plugin = "akka.persistence.snapshot-store.sql"
    sql {
      class = "Akka.Persistence.Sql.Snapshot.SqlSnapshotStore, Akka.Persistence.Sql"
      connection-string = "<redacted>"
      provider-name = "SqlServer2019"
    }
  }
}

Akka.Persistence.Sql default table mapping used by this profile:

Purpose Table Important columns
event journal journal ordering, deleted, persistence_id, sequence_number, created, tags, message, identifier, manifest, writer_uuid
journal metadata journal_metadata persistence_id, sequence_number
tag table tags ordering_id, tag, persistence_id, sequence_nr
snapshot store snapshot persistence_id, sequence_number, created, snapshot, manifest, serializer_id

The built-in Journal.sqlServerQueryHealthAdapter() intentionally probes only the default journal table and reads:

SELECT MAX(CAST([sequence_number] AS bigint))
FROM [<schema>].[journal]
WHERE [persistence_id] LIKE @persistenceIdPrefix
  AND ([deleted] = 0 OR [deleted] IS NULL);

PCSL remains projection/cache. PCSL roots and projection metadata are not Akka snapshot tables.

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。

Participant registry 是 identity catalog 加 lifecycle stream,不是永久 online 清單。participant.registered 會刷新一小時 presence lease;未知 chat peer只寫 participant.discovered 並保持 offline;participant.retired 明確退休 identity。registry首次讀取以 ReadTail + ReadBefore 完整 replay,後續按 sequence增量追平,避免 1000-event tail截斷與每次 request全量重播。

Clean Inactive Participant CollectionsCleanAllNoShow Actors 是兩個不同 admin action。前者只處理 inactive/retired、超過 retention且 inbox已 fully acked 的 participant inbox/ack stream;它追加 set.stream.hidden tombstone,不刪 journal。Browser端在 tombstone完成後以 IndexedDB transaction清除目前 server reality全部 sets-state snapshot/watermark,且忽略晚到但時間早於 tombstone 的 WebSocket frame;後續新 message仍可 revive stream。

真正 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 default agent-task handoff;
  • SubmitAgentTaskDurableWithOptionsAsync(..., MessageFabricAgentTaskSubmissionOptions.withoutMessageFabricProjection) 保留durable ticket/task identity/result query contract,但不把內部task投影為participant MessageFabric message;
  • 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.5-beta116"

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

Actor Registry Generation / Lease

啟用CommSpaActorFabricOptions.ActorRegistry時,ActorFabric會為sharding region/proxy持有ActorRegistryShardingLeaseHandle。事件寫入Host提供的sink;PTCS以PCSL actor-registry stream保存lifecycle history。heartbeat只追加canonical registry stream,不重複投影legacy generic set,也不在sink內建立無人使用的snapshot。正常CommSpaActorFabric.Stop()發出StoppingUnregistered/Stopped,非正常終止則由最後heartbeat加lease判定stale。

Host應固定sharding type,並為每次process start指定新的generation。Actors API default view只回current active generation;?includeOffline=true保留stale/stopped診斷。Browser IndexedDB不是truth,Reload以後端snapshot取代current view。

Generation selection先以NodeId + registry scope選最新Registered generation,再於該generation內依actor identity取最新lifecycle。2026-09-06以前的PTCS Host random extension region沒有registry:/role: tag;PTCS只針對同時符合/system/sharding/ptcs-ext-host-*actor-fabricdynamic-extension的舊格式映射到ptcs-host-extension-fabric scope。PCSL history不刪除,但active view只顯示ptcs-extension-stream。啟動時逐筆retirement只可作best-effort診斷修正,不是projection正確性的必要條件。

詳見doc/RFC/RFC-PTC-SPA-0024.actor-registry-generation-lease-reconciliation.md

Durable Group Chat

PTCS MessageFabric是group identity、owner/admin/member、membership intervals與history visibility的authority。一般group使用append-only lifecycle;channel.public是immutable system group。第一次加入可由owner選擇是否包含先前history,但participant離開到重加入期間的message永遠不可見。

正式整合使用explicit create/member/role/ownership APIs;MessageFabricGroupUpsert僅保留legacy compatibility。Server從authenticated session取得requester,先做optional ACL preflight,再由MessageFabric在append commit前重驗group role與revision;兩層都fail closed,ACL只能再收窄。GW/MCP只轉送typed command,不保存group projection。完整契約見doc/RFC/RFC-PTC-SPA-0027.durable-group-chat-lifecycle.md

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

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

Package Downloads
PulseTrade.Comm.Spa.Dynamic

Dynamic SDUI and Actor extension for PulseTrade.Comm.Spa

PulseTrade.Comm.ResourceNode.DurableProxy

Package Description

PulseTrade.Comm.Spa.Dynamic.Ptcs.Client

Pure WebSharper same-origin PTCS transient client adapter for Dynamic TA Research.

PulseTrade.Comm.Spa.Dynamic.Ptcs

PTCS same-session transient adapter for PulseTrade Dynamic SDUI runtime.

PulseTrade.Comm.ResourceNode.Pcsl

Package Description

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.2.32 0 9/16/2026
0.2.31 0 9/16/2026
0.2.30 0 9/16/2026
0.2.29 0 9/16/2026
0.2.28 0 9/16/2026
0.2.27 0 9/15/2026
0.2.26 32 9/15/2026
0.2.25 21 9/15/2026
0.2.24 29 9/15/2026
0.2.23 60 9/15/2026
0.2.22 87 9/14/2026
0.2.21 101 9/14/2026
0.2.20 123 9/13/2026
0.2.19 102 9/13/2026
0.2.18 306 9/8/2026
0.2.17 156 9/8/2026
0.2.16 141 9/8/2026
0.2.15 187 9/8/2026
0.2.14 134 9/7/2026
0.2.13 109 9/7/2026
Loading failed