mirror of
https://github.com/wu736139669/hapi.git
synced 2026-10-20 21:26:04 +00:00
HapiProtocol/Window/ — pure port of web/src/lib/message-window-store.ts + messages.ts, mirroring the Android reference port 1:1: - MessageWindowState: constants (400/600/800/800/200), MessagePosition, OlderLoadOutcome, full InternalState fields, persisted v2 snapshot shape - MessageWindowLogic: every transition (merge/trim preserving queued rows, latest replace with request-baseline identity preservation, tail sync begin/apply/finish, older pages + epoch-mismatch reset, enterTailMode that deliberately keeps requiresLatestReset, activate, SSE ingest with hidden-row cursor advance, markConsumed stamping server rows to 'sent', optimistic lifecycle, queued reconcile, hydrate/persist, seededState) - MessageMerge: position comparator (ASCII tie-break), localId echo replacement preserving status/invokedAt, 10s sent-dedup fallback - MessageRetention: calls the fixtures-green chat pipeline's normalizeDecryptedMessage directly (no hand-mirrored tree to drift) - WindowMessage: identity-carrying final class (web's !== baseline classification), tri-state invokedAt, buildOptimisticMessage HapiClient/Stores/ — async half: - MessagesProviding seam (three-variant MessagesPageQuery; APIClient conforms via its existing endpoints) - MessageWindowController actor: single-flight tail controller with trailing drain and a synchronous generation bump before the first await (the web runs to its first suspension; Android used UNDISPATCHED), fetchOlder with onBeforeApply veto, SSE hooks, optimistic append/status/ cancel-invoked, queued-state reconciliation in 1000-id batches, seedFrom - WindowSnapshotStore: per-session JSON snapshots, LRU 10 (dedup TODO with the session-list package's cache) + MessageWindowControllers registry Tests (swift-testing): - PaginationFixtureTests: parameterized replay of shared/fixtures/ pagination/*.json against the real controller via a scripted provider; asserts expectedRequests (canonical JSON incl. explicit-null untils), expectedOutcome, expectedCandidates, and the final expectedState projection with per-op labels and first-differing-line diffs - MessageWindowControllerTests: tail-sync coalescing + trailing drain, concurrent-SSE preservation across a reset replace, cursor-no-advance guard, snapshot round-trip/LRU, seedFrom All 11 fixtures verified green against a line-by-line Python mirror of these exact algorithms (no local Swift toolchain; CI runs the real suite).
63 lines
2.8 KiB
Swift
63 lines
2.8 KiB
Swift
import Foundation
|
|
import HapiProtocol
|
|
|
|
/// One `GET /api/sessions/:id/messages` request. The three variants are the
|
|
/// only shapes the reference client ever issues
|
|
/// (`docs/api/client-contract/pagination.md`): the window controller cannot
|
|
/// produce an invalid parameter combination by construction, and the
|
|
/// pagination fixture harness asserts these against the recorded
|
|
/// `expectedRequests` — `.after` always carries the `until` keys (explicitly
|
|
/// null on the first catch-up request), the other variants never do.
|
|
/// Mirror of the Android port's `MessagesQuery` sealed interface
|
|
/// (`app/hapi/data/api/MessagesApi.kt`).
|
|
public enum MessagesPageQuery: Equatable, Sendable {
|
|
/// Newest page, no cursor.
|
|
case latest(limit: Int)
|
|
/// Rows strictly older than the compound cursor.
|
|
case before(beforeAt: Int, beforeSeq: Int, limit: Int)
|
|
/// Tail catch-up: rows strictly newer than the cursor, snapshot-bounded.
|
|
case after(afterAt: Int, afterSeq: Int, untilAt: Int?, untilSeq: Int?, epoch: Int, limit: Int)
|
|
|
|
/// The flat REST query (`MessagesQuerySchema`) this variant serializes
|
|
/// to. Null `until` halves are omitted from the URL, like the web client.
|
|
public var restQuery: MessagesQuery {
|
|
switch self {
|
|
case .latest(let limit):
|
|
return .latest(limit: limit)
|
|
case .before(let beforeAt, let beforeSeq, let limit):
|
|
return .before(seq: beforeSeq, at: beforeAt, limit: limit)
|
|
case .after(let afterAt, let afterSeq, let untilAt, let untilSeq, let epoch, let limit):
|
|
return .after(
|
|
seq: afterSeq,
|
|
at: afterAt,
|
|
epoch: epoch,
|
|
untilSeq: untilSeq,
|
|
untilAt: untilAt,
|
|
limit: limit
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Minimal seam over the two message endpoints the window controller drives —
|
|
/// implemented by ``APIClient`` in production and by a scripted fake in the
|
|
/// pagination fixture harness. Extracted (instead of depending on the
|
|
/// concrete client) so the REAL orchestration is what the fixtures replay.
|
|
/// Mirror of the Android port's `MessagesApi`.
|
|
public protocol MessagesProviding: Sendable {
|
|
/// `GET /api/sessions/:id/messages`.
|
|
func messages(sessionId: String, query: MessagesPageQuery) async throws -> MessagesResponse
|
|
|
|
/// `POST /api/sessions/:id/messages/queued-state`.
|
|
func queuedState(sessionId: String, localIds: [String]) async throws -> QueuedStateResponse
|
|
}
|
|
|
|
extension APIClient: MessagesProviding {
|
|
public func messages(sessionId: String, query: MessagesPageQuery) async throws -> MessagesResponse {
|
|
try await messages(sessionId: sessionId, query: query.restQuery)
|
|
}
|
|
|
|
// `queuedState(sessionId:localIds:)` from Endpoints/MessageEndpoints.swift
|
|
// already satisfies the second requirement.
|
|
}
|