@bimetal/sync
Sync-Primitive (v0.15) für BIMETAL. Ein strukturelles, framework-agnostisches
SyncTransport-Interface plus eine In-Memory-Loopback-Implementierung — die Grundlage für
Multi-Client-Synchronisation über eine Server-Autorität.
Das Modell ist bewusst schlank: Server-Autorität, Optimistic Locking, First-Writer-Wins. Keine
Resolver, kein previous-Feld, kein Last-Write-Wins, keine Timestamp-Tiebreaker, keine Merges im Kern —
ein Konflikt wird laut (ConcurrencyError), nicht still aufgelöst. Fachliche Merge-/Recovery-Strategien
gehören in die App, nicht ins Framework.
Installation
npm install @bimetal/sync @bimetal/event-sourcing
Was das Paket liefert
| Export | Zweck |
|---|---|
SyncTransport | Das strukturelle Transport-Interface (senden/empfangen von Sync-Nachrichten) — ein WebSocket-Adapter (@bimetal/sync-ws) implementiert es. |
createSyncedEventStore(options) | Composer (kein Replacement): wickelt einen lokalen EventStore in einen sync-fähigen Store — lokale Appends gehen über den Transport an die Autorität, Remote-Events werden eingefoldet. Konfiguration über ein SyncedEventStoreOptions-Objekt. |
SyncCatchUp | Wie aufgeholt wird — Pflichtfeld catchUp, ohne Default: { kind: 'full' }, { kind: 'since', version } oder { kind: 'snapshot', snapshotStore }. Diskriminierte Union, damit eine vierte Anbindungsart keine Aufrufstelle bricht. |
SyncedEventStore.caughtUp | Barriere für handelnde Konsumenten. Danach ist kein zweites hydrate() nötig — der Catch-up ist gefaltet, wenn sie settelt; einzige Ausnahme ist { kind: 'snapshot' }, wo genau ein hydrate() den abgelegten Snapshot übernimmt. Ein Catch-up-Frame wird EIN lokaler append und EINE Benachrichtigung je DomainStore, nicht eine je Event (bimetal-257). |
SyncTransport.undo / .redo | Das Undo-Kommando an die Autorität (undo(streamId, entityId, options?)) — „nimm zurück, was irgendwer geändert hat", auch vor meinem Seitenaufruf. Nicht der lokale Undo-Stapel: der bleibt im Store und geht diesen Weg gar nicht. Die Autorität faltet ihr Aggregat und appendet die Umkehrung; nichts wird gelöscht oder zurückgespult. Der Promise settelt mit der Antwort und liefert die Kopf-Version; eine Ablehnung mit Grund concurrency (überholte expectFrame-Marke) wirft denselben ConcurrencyError wie ein abgewiesener append, mit den Marken im Text — jede andere (etwa Autorität ohne undoDomain, Grund unknown) einen schlichten Error mit dem Ablehnungstext. Beide Methoden sind optional — ein fremder Transport ohne Undo-Weg erfüllt das Interface weiter, Konsumenten prüfen auf Vorhandensein. |
SyncUndoOptions | { expectFrame? } — Frame-Genauigkeit (bimetal-155) über den Draht. Die Marke ist die des AUTORITÄTS-Standes (envelope.id des jüngsten Events dieser Entität im Stream), nicht die prozesslokale aus DomainStore.peekUndoFrame(). Passt sie nicht mehr, weist die Autorität ab statt die fremde Änderung zurückzunehmen. Weglassen ist der Normalfall für einen Client ohne Log. |
SyncUndoDomain | Der Domänen-Anteil, den eine Autorität für das Kommando braucht: Aggregat + buildUndoCommand/buildRedoCommand (+ optional generateId/now). Ein Port, kein Import — das Paket kennt keine Aggregate; die Mechanik (falten, umhüllen, anfügen, ausliefern) bleibt bei der Autorität. |
ClientMessage | Wire-Union der Client-Nachrichten: append, subscribe, unsubscribe, ping sowie undo / redo ({ requestId, streamId, entityId, expectFrame? }). Die Antwort auf ein Kommando ist append-ok / append-rejected, zugeordnet über requestId. |
createInMemorySync() / createInMemorySyncAuthority() | Loopback-Transport + In-Memory-Autorität — für Tests und Single-Prozess-Szenarien. Mit undoDomain bedient die Autorität auch undo/redo. |
InMemorySyncAuthority | Der Autoritäts-Typ. |
Architektur
Cross-cutting (Transport). Konzepte: Event-Sourcing-Konzept und Backend-Überblick — dort auch „Wie groß wird das?“, die Grenze des Log-im-Client (Optimistic Locking / Concurrency).