🎉 2.0 is here — the target-based API is the recommended way to use swift-kurrentdb. Already on 1.x? Use KurrentDB_V1 to keep your existing code while you migrate.
Typed errors for deleted streams and conflicts, and KurrentDB 26.2刪除的 stream 與版本衝突改成型別化錯誤,並支援 KurrentDB 26.2
2.6 turns server errors that used to surface as raw gRPC errors into typed
KurrentErrors, adds KurrentDB 26.2 to the tested servers, and documents the
source and license of every bundled .proto file. The public API is source
compatible with 2.5; read the upgrade notes if you match errors from delete,
tombstone or a buffered read().
A read() the server rejects now throws the same KurrentError as read().lazy: resourceDeleted for a tombstoned stream, accessDenied without permission. delete and tombstone report a revision conflict as wrongExpectedVersion(expected:current:), the same case append throws, with current set to .noStream when the stream does not exist.
do {
try await client.streams(specified: "orders").delete {
$0.expectedRevision = .at(3)
}
} catch KurrentError.wrongExpectedVersion(_, let current) {
// current is .noStream or .revision(n)
}
KurrentDB 26.2KurrentDB 26.2
CI now tests KurrentDB 26.2 across Swift 6.0–6.4, on a single TLS node: 26.2 needs a Kurrent license to run a multi-node cluster. X.509 user certificates are tested on every run against an unlicensed 26.2 node. 26.2 rejects creating transient projections as deprecated.
KurrentError.CurrentRevisionOption is renamed CurrentRevisionState, because it is the server's observed state, not an option. The old name remains as a deprecated typealias.
The license file is now LICENSE, and a new NOTICE records the source and license of every bundled .proto file. The unused schema registry protos, the only ones under the Kurrent License v1, are removed; they were package-level only.
A buffered read() the server rejects throws resourceDeleted or accessDenied instead of a raw RPCError, still while iterating. Errors after the server accepted the call are unchanged.
Reads that stream, and inputs that can't hurt you邊讀邊用的讀取,以及傷不了你的輸入
2.5 lets a read hand you events as you iterate instead of loading the whole result first,
puts a bound on subscription memory, and closes every finding of a security review —
connection strings, credentials, CA certificates and server responses can no longer crash
the process, leak a password, or quietly weaken TLS. The public API is source compatible
with 2.4, but some inputs 2.4 accepted now throw: read the upgrade notes below.
Lazy reads and bounded subscriptionsLazy 讀取與有上限的訂閱
read().lazy pulls events from the server as your loop consumes them, and the call ends as soon as the loop does — on completion, break, a thrown error or cancellation. Plain read() keeps the 2.4 behaviour (the whole result first). Subscriptions now wait for your code instead of piling events up in memory.
for try await response in client.streams(specified: "orders").read().lazy {
// events arrive as you iterate; memory does not grow with the stream
}
Strict connection strings嚴格的連線字串
Connection strings are parsed according to RFC 3986. A character in your password can no longer turn off TLS or point the client at another host; unknown, duplicate or invalid parameters throw instead of being ignored; and error messages name the parameter, never its value. Percent-encode a password that contains @ / ? # % — or pass it with .authenticated(.credentials(username:password:)) and skip the connection string.
defaultDeadline now takes effect for calls that complete before they return (appends, deletes, management calls, read()); subscriptions and read().lazy are not bounded by it.
A CA certificate that can't be read is reported instead of silently trusting the system's root certificates. Use certificate(path:fallback:); certificate(path:) is deprecated.
CA 憑證讀不到時會直接報錯,不再悄悄改成信任系統的根憑證。請改用 certificate(path:fallback:);certificate(path:) 已標成 deprecated。
Negative revisions or positions from the server and credentials that can't be encoded fail with an error instead of crashing the process or sending the request anonymously.
Connection strings with unknown parameters (including other clients' parameters such as throwOnAppendFailure), malformed values, or unencoded @ in a value (write %40) now throw.
Connection reuse — faster and more solid連線復用 —— 更快,也更穩
Up to 2.3, every single call — each append, each read, each delete — dialled a brand-new
connection to the server: a TCP handshake, a TLS handshake, the request, then hang up.
2.4 keeps the line open. Same API, nothing to change in your code; it just gets faster
and holds up better.
Think of it like phone calls. Before, it was like hanging up and redialling after every sentence. Now there's one open line everyone takes turns on — HTTP/2 was built to carry many requests over one connection at once. Callers who talk for hours (subscriptions) get their own line, so they never tie up the shared one.
Which line a call takes depends on what comes back: one response → the shared line; a stream of responses → its own line.走哪條線,看回來的是什麼:只回一個結果的,走共用線;會一直回傳資料的(stream),走自己的線。
⚡️ Faster⚡️ 更快在哪
Far fewer handshakes. A TLS handshake takes several network round trips before a single byte of your request moves. A page that does 200 appends used to pay for 200 of them; now it pays once.
Cluster discovery reuses it. Gossip and server-feature checks go over the same shared connections instead of opening their own.
叢集探索也共用。gossip 與伺服器功能檢查也走同一批共用連線,不再各開各的。
Verified, not assumed. Our integration tests check that after warm-up, 200 back-to-back appends open zero new connections.
有測試撐腰。整合測試會實際驗證:暖機之後連續 200 次 append,一條新連線都沒開。
🛡️ More solid🛡️ 更穩在哪
Subscriptions never clog the line. Each one has its own connection, so a pile of long-running subscriptions can't make your appends wait in line. Tested with 150 subscriptions open at once while appends still go through.
Cancel means stop. Cancelling a subscription used to leave the network read running in the background. Now it really stops.
取消就是真的停。以前取消訂閱後,底層的網路讀取還會在背景偷偷跑;現在是真的停下來。
Blips heal themselves. If the network hiccups or the server restarts, the connection reconnects on its own. One slow call timing out doesn't tear down the line everyone else is using.
Dead connections get noticed. Your keepAliveInterval / keepAliveTimeout settings are now actually applied, so a connection that silently died while calls are in flight is detected instead of hanging.
Opened建立on the first call to that node第一次打到這個 node 時
In use使用中any number of calls share it多少個呼叫都一起用
Idle閒置no calls in flight目前沒有呼叫在跑
Retired回收after 1 hour idle閒置超過 1 小時
Reopened重新建立on the next call, automatically下次呼叫時自動重開
A connection is never retired while a call is using it — the client keeps count.
只要還有呼叫在用,連線就不會被回收 —— client 會記著目前有幾個人在用。
What you need to do你需要做什麼
Almost nothing. Create one client for your whole app and share it — creating a new client per request means connections can't be shared.
幾乎什麼都不用做。整個 app 建一個 client、到處共用就好 —— 如果每個請求都 new 一個 client,連線就沒辦法共用了。
// One client for the whole app
let client = KurrentDBClient(settings: settings)
// When the app is shutting down
try client.shutdown()
2.4.1: dropped subscriptions hang up2.4.1:訂閱丟掉就掛線
A persistent subscription's connection now closes as soon as nothing references the subscription or its events stream — even if you never iterated it. Before 2.4.1 a retain cycle kept those connections open.
2.4.2: retries that actually wait, and X.509 that actually sends2.4.2:重試真的會等,X.509 真的會送
Settings built in code (.localhost(), .remote(), ClientSettings(...)) waited 100 µs between node-discovery retries instead of 100 ms, so a leader election failed calls at once. They now wait 100 ms, like connection strings.
Every Swift example in the docs is now compiled in CI.
文件裡的每個 Swift 範例現在都會在 CI 編譯檢查。
2.4.3: reads share the connection2.4.3:讀取共用連線
read(), readAll(), projection statistics and user details now use the per-node shared connection instead of opening a TCP connection for every call. Only subscriptions, persistent subscriptions and server statistics keep a dedicated one.
read()、readAll()、projection statistics 與 user details 現在走每個節點的共用連線,不再每次呼叫都開一條 TCP 連線。只有訂閱、persistent subscription 與 server statistics 維持獨立連線。
A burst of reads beyond the server's concurrent-stream limit queues on the shared connection alongside appends instead of opening more sockets.
A new library for tests: borrow a client from a pool of independent KurrentDB instances with withBorrowedClient(_:), configured through the KURRENTDB_POOL_URLS environment variable (a JSON array of connection strings).
ClientSettings.endpointResolutionPreference states whether to use the gossip-reported address (.gossipReported, the default for seeds and DNS) or the one you configured (.userConfigured, the default for standalone). This stops endpoints collapsing onto the same address when several local containers share 127.0.0.1.
Default timeouts were in milliseconds where seconds were intended: messageTimeout is now 30 s (was 30 ms) and checkpointAfter 2 s (was 2 ms), matching the server defaults.
預設逾時值原本誤用了毫秒:messageTimeout 從 30 ms 改為 30 秒,checkpointAfter 從 2 ms 改為 2 秒,與伺服器預設一致。
Build client settings straight from an environment variable holding a connection string — SWIFT_KURRENT_DB_URL by default. Handy for containers and CI.
直接從存放連線字串的環境變數建立 client 設定,預設讀 SWIFT_KURRENT_DB_URL。在 container 與 CI 裡特別方便。
let settings = try ClientSettings.fromEnv()
let custom = try ClientSettings.fromEnv(key: "MY_KURRENTDB_URL")
🔧 getMetadata()
Now returns nil instead of throwing when the stream has no metadata yet, as documented.
2.1 builds on the 2.0 target-based API with new write and read capabilities, per-call
authentication, and a hardened persistent-subscription connection lifecycle.
2.1 在 2.0 的 target-based API 上,新增寫入與讀取能力、逐請求認證,並強化了持久訂閱的連線生命週期。
🧬 AppendRecords (Dynamic Consistency Boundary)
Atomically append across multiple streams with cross-stream consistency checks. Requires server support (KurrentDB 26.1+).
Connection tasks are now owned and torn down deterministically, connection failures are observable, and node-level drops surface as subscriptionDropped with resume position.
Version 2.0 is a ground-up redesign of the public API. The flat
client.appendToStream(...) surface gives way to typed targets the compiler can verify —
illegal operations literally don't compile.
2.0 是 API 的全面重新設計。原本 client.appendToStream(...) 的扁平呼叫,
換成編譯器能驗證的型別化 target —— 不合法的操作根本通不過編譯。
🎯 Target-based API
Every operation flows through a typed target. Tombstoning $all? Reading from a write-only target? Won't compile.
import KurrentDB
// Local development — single node
let settings = ClientSettings.localhost()
.authenticated(.credentials(username: "admin", password: "changeit"))
// Production — remote cluster (TLS enabled by default)
let production = ClientSettings.remote(
"node1.example.com:2113",
"node2.example.com:2113",
"node3.example.com:2113"
).authenticated(.credentials(username: "admin", password: "changeit"))
let client = KurrentDBClient(settings: settings)
Append & Read Events寫入與讀取事件
struct OrderPlaced: Codable, Sendable {
let orderId: String
let total: Double
}
// Create an event
let event = EventData(
eventType: "OrderPlaced",
model: OrderPlaced(orderId: "order-123", total: 99.99)
)
// Append to stream
try await client.streams(specified: "orders").append(events: [event]) {
$0.expectedRevision = .any
}
// Read events
let responses = try await client.streams(specified: "orders").read {
$0.revision = .start
$0.limit = 10
}
for try await response in responses {
let readEvent = try response.event
print("Event: \(readEvent.record.eventType)")
}
Subscribe訂閱
// Catch-up subscription
let subscription = try await client.streams(specified: "orders").subscribe()
for try await readEvent in subscription.events {
print("Live event: \(readEvent.record.eventType)")
}
API at a glanceAPI 一覽
The target-based design scopes every operation through a typed target — the compiler keeps you on rails.
Core features · no multi-stream append or AppendRecords核心功能 · 無多 stream 原子寫入與 AppendRecords
Tested in CI across Swift 6.0 / 6.1 / 6.2 / 6.3 / 6.4 against every supported server version.
CI 在 Swift 6.0 / 6.1 / 6.2 / 6.3 / 6.4 對所有支援的伺服器版本進行完整整合測試。
Benchmarks效能基準
Offline benchmarks run on every push to main on Linux and are recorded on Bencher, one point per commit. The charts below are live; click one to open the interactive view. Full results for macOS and Linux, environment and method: Benchmarks page.
離線效能基準每次 push 到 main 都會在 Linux 上執行並記錄到 Bencher,每個 commit 一個資料點。下方圖表即時更新,點擊可開啟互動頁面。macOS 與 Linux 的完整數據、量測環境與方法見效能基準頁面。