synckit

package
v0.5.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 9, 2026 License: MIT Imports: 8 Imported by: 0

Documentation

Overview

Package synckit is the Go client half of a bidirectional, last-write-wins sync protocol. It shares an exact wire contract with the server engine and with the protocol's other client implementations: a single POST carries both directions —

request : { since: <lastServerRev>, changes: [<local dirty Records>] }
response: { server_rev: <new cursor>, records: [<everything rev > since>],
            conflicts: [<device changes that lost LWW>] }

The Record/Conflict/Meta/Request/Response types here are byte-compatible with the server's, and Wins() is a copy of the server's wins() LWW rule so a client can reason about conflicts identically. Deliberately minimal: this package does NOT own the apply-then-advance-cursor / dirty-tracking loop — that belongs to the consuming agent, which must only advance its `since` cursor after a successful, fully-applied Pull.

Index

Constants

This section is empty.

Variables

View Source
var ErrProtocol = errors.New("synckit: protocol violation")

ErrProtocol marks a server answer that violates the paging contract. The page is NOT passed to apply, so nothing about it is persisted.

View Source
var ErrResync = errors.New("synckit: resync required")

ErrResync is returned by PullPaged when the server reports ResyncRequired.

The server REFUSES the whole call on resync: the first page's changes were NOT applied and any Deduped it carried is meaningless. The caller must discard its CLEAN mirror rows only (never its queued, unpushed writes), reset its cursor to 0, and call PullPaged again re-sending the same changes. PullPaged never wipes anything itself.

Functions

func PullPaged added in v0.5.0

func PullPaged(ctx context.Context, pull PullFunc, since int64, changes []Record, limit int,
	apply func(Response) error) (int64, error)

PullPaged drives the paged protocol: the first request carries the dirty changes, follow-ups carry an empty (non-null) changes list and resume from the previous page's ServerRev.

Every page is validated BEFORE apply, so a bad page never reaches storage. apply is called once per page and must durably apply it AND persist the cursor (resp.ServerRev) in the same step; the next page is requested only after apply returns nil, so a crash never advances the cursor past unapplied records. Returns the final cursor.

func Wins

func Wins(inc Record, cur Meta) bool

Wins reports whether an incoming record should overwrite the current stored row under last-write-wins. IDENTICAL to the server engine's wins(): strictly-newer updated_at wins; on an exact updated_at tie a delete beats a non-delete (a tombstone is never silently lost to a same-instant edit); any other tie keeps the existing row (deterministic, no flapping).

Types

type Conflict

type Conflict struct {
	Kind            string    `json:"kind"`
	ID              string    `json:"id"`
	DeviceUpdatedAt time.Time `json:"device_updated_at"`
	ServerUpdatedAt time.Time `json:"server_updated_at"`
}

Conflict records a local change that lost the last-write-wins comparison (the server already had a strictly-newer version). Surfaced so the caller can log it; the client is expected to reconcile from the returned records.

type Deduped added in v0.5.0

type Deduped struct {
	ID     string  `json:"id"`
	Status string  `json:"status"`
	Of     string  `json:"of"`
	Score  float64 `json:"score"`
}

Deduped reports a pushed record the server dropped because its (repo, content_hash) matched a live row under another id. It is keyed on the CLIENT's id. Mirror rule: tombstone the local ID locally (never pushed) and adopt the server row Of.

type Meta

type Meta struct {
	Rev       int64
	UpdatedAt time.Time
	Deleted   bool
}

Meta is the sync-metadata of a locally-stored row, used for the LWW decision.

type PullFunc added in v0.5.0

type PullFunc func(ctx context.Context, req Request) (Response, error)

PullFunc performs one push+pull round-trip (production: a closure over Pull).

type Record

type Record struct {
	Kind      string         `json:"kind"`
	ID        string         `json:"id"`
	Rev       int64          `json:"rev"`
	UpdatedAt time.Time      `json:"updated_at"`
	Deleted   bool           `json:"deleted"`
	UpdatedBy string         `json:"updated_by,omitempty"`
	Fields    map[string]any `json:"fields,omitempty"`
}

Record is one syncable row on the wire, in either direction.

  • Kind identifies which logical table the row belongs to (e.g. "host", "key") so one stream can carry several kinds under a single `since`.
  • Rev is the server-assigned monotonic version; clients send 0 on input (the server assigns it) and echo it back from pulled records.
  • Fields holds the app data columns keyed by column name. On the wire it is whatever JSON produced (numbers as float64, bytea as base64 strings, …).

type Request

type Request struct {
	Since   int64    `json:"since"`
	Changes []Record `json:"changes"`
	Limit   int      `json:"limit,omitempty"`
}

Request is the sync POST body: the client's last server cursor plus its locally-dirty records to push.

Limit (omitted when 0) asks the server to page: it returns the Limit lowest-rev records with rev > Since and sets Response.More. Zero keeps the legacy unbounded behaviour, so clients that never set it are unchanged.

type Response

type Response struct {
	ServerRev      int64      `json:"server_rev"`
	Records        []Record   `json:"records"`
	Conflicts      []Conflict `json:"conflicts,omitempty"`
	Deduped        []Deduped  `json:"deduped,omitempty"`
	More           bool       `json:"more,omitempty"`
	ResyncRequired bool       `json:"resync_required,omitempty"`
	MinRetainedRev int64      `json:"min_retained_rev,omitempty"`
}

Response is the sync POST result: the new server cursor, every row with rev > since, and any pushed changes that lost LWW.

Paging fields are omitted by servers that do not page. With More set, ServerRev is the last rev returned (not the high-water) and the client re-POSTs with Since=ServerRev. ResyncRequired means Since fell below MinRetainedRev (tombstones swept) and the server REFUSED the whole call: nothing was applied, no Records are returned. The client discards its CLEAN mirror rows only (never queued, unpushed writes), resets its cursor to 0 and re-sends the same changes on the Since=0 call.

func Pull

func Pull(ctx context.Context, httpClient *http.Client, url string, req Request) (Response, error)

Pull performs one push+pull round-trip: it POSTs req as JSON to url and decodes a 2xx JSON body into a Response. A non-2xx status returns an error and an empty Response WITHOUT decoding a body, so the caller knows NOT to advance its `since` cursor. Applying the returned records/conflicts and advancing the cursor only after a fully-successful apply is the caller's job.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL