first commit
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
# `StateReconciler`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Resolves the five reconciliation strategies (`latest-wins`, `remote-wins`, `local-wins`, `deep-merge`, `custom`) against a single `ConflictInput` tuple and returns a typed `ConflictResolution`. The path strategy registry and sync engine consult the reconciler — the logic is pure so consumers can also call it directly for tests and audits.
|
||||
|
||||
## Features
|
||||
|
||||
- Closed `ReconciliationStrategy` switch with `winner` reported on every resolution (`local` / `remote` / `merged`).
|
||||
- `latest-wins` ties go to the remote so the receiver does not stall when timestamps coincide.
|
||||
- `deep-merge` recursively combines plain objects; scalar / array conflicts deterministically pick the remote so the merge is reproducible.
|
||||
- `custom` strategy delegates to a `CustomMergeFn`; absent merge falls back to the local value (caller bug — registry rejects this at registration time so this is defensive only).
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
StateReconciler,
|
||||
type ConflictInput,
|
||||
} from "@suaveplan/network-plugin-state";
|
||||
|
||||
const r = new StateReconciler();
|
||||
const input: ConflictInput = {
|
||||
path: "score",
|
||||
localValue: 1,
|
||||
localTimestamp: 1,
|
||||
remoteValue: 2,
|
||||
remoteTimestamp: 2,
|
||||
};
|
||||
const out = r.resolve("latest-wins", input);
|
||||
// out.resolvedValue === 2, out.winner === "remote"
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
StateReconciler,
|
||||
deepMerge,
|
||||
type ConflictInput,
|
||||
} from "@suaveplan/network-plugin-state";
|
||||
|
||||
const r = new StateReconciler();
|
||||
const input: ConflictInput = {
|
||||
path: "inventory",
|
||||
localValue: { coins: 10, gems: 1 },
|
||||
localTimestamp: 100,
|
||||
remoteValue: { coins: 5, scrolls: 2 },
|
||||
remoteTimestamp: 99,
|
||||
};
|
||||
const dm = r.resolve("deep-merge", input);
|
||||
// dm.resolvedValue === { coins: 5, gems: 1, scrolls: 2 } — remote wins on conflict, locals merged in
|
||||
|
||||
const merged = deepMerge(input.localValue, input.remoteValue);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `class StateReconciler`
|
||||
- `resolve(strategy, input, merge?): ConflictResolution`.
|
||||
- `function deepMerge(local, remote): StateValue | undefined` — exported pure helper for consumers that want the merge result without the wrapper.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The reconciler is stateless — every `resolve` call is pure given its inputs. Subscribers can run resolutions in parallel without coordination.
|
||||
- `latest-wins`'s tie-break (remote on equal timestamps) avoids ping-pong cycles where two peers keep "winning" the same conflict at the same time.
|
||||
- `deep-merge`'s scalar-tie remote-wins rule means the merge is associative — `deepMerge(a, b) === deepMerge(deepMerge(a, b), b)` for all inputs — which is essential for consumers that re-apply the merge under retries.
|
||||
- `custom` strategy without a `merge` is defensive: the registry's `addStrategy` raises `STRATEGY_MISSING_MERGE` at registration, so the in-resolve fallback only activates if a caller bypasses the registry.
|
||||
Reference in New Issue
Block a user