@openlfcp/client 0.1.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,203 @@
1
+ import { type ControlRecordId, type DataEpoch, type DataUnitId, type Hash32, type ResourceId } from "@openlfcp/core";
2
+ import { type AgreementKeyPair } from "@openlfcp/crypto";
3
+ import { type LfcpStorage, type SecretStore } from "@openlfcp/storage";
4
+ import { type ClientConnectionState, type DataProfileCodec, type HaveVector, type Signer } from "@openlfcp/wire";
5
+ import type { ApplyOutcome, DataUnitApplier, EpochReconciliation } from "./apply.js";
6
+ import type { ProfileCheckpointer } from "./checkpoint.js";
7
+ import { type WebSocketFactory } from "./connection.js";
8
+ import type { AckOutcome, NackOutcome, OutboundQueue, StaleOutboundUnit } from "./outbound.js";
9
+ import { type ResourcePhase } from "./resource-state.js";
10
+ /**
11
+ * The client sync session (LFCP-039a): one LFCP connection to a server and
12
+ * the §65 machine of every Resource opened on it, orchestrating the parts
13
+ * built before it. It adds no protocol rule of its own:
14
+ *
15
+ * - connection, handshake, heartbeat, size limits: LfcpConnection;
16
+ * - Control catch-up: planControlSync, then validateControlChain over the
17
+ * stored records and the fetched ones; a fork is stored and surfaced
18
+ * (CONTROL_CONFLICT), never resolved;
19
+ * - keys: KEY_PACKAGE_GET for epochs without a DEK, receiveKeyPackage, the
20
+ * DEK into the SecretStore before its reference into storage;
21
+ * - data: Have Vectors (missingFrom, ≤256 ranges per DATA_GET), every unit
22
+ * through the DataUnitApplier; periodic DATA_HAVE (§69: anti-entropy);
23
+ * - outbound: OutboundQueue, flushed once keys are in place (§88 step 6),
24
+ * ACK and NACK routed back to it;
25
+ * - on every newly validated Control state: DataUnitApplier.reconcileEpochs
26
+ * AND OutboundQueue.reconcileEpochs (G-EP7, §88 step 7).
27
+ *
28
+ * Time and timers belong to the caller: tick(now) does the periodic work
29
+ * (heartbeat, anti-entropy, retries, checkpoints, reconnect), and a thin
30
+ * driver may call it from a timer (startSyncDriver). Events tell the
31
+ * application what happened; nothing is logged, plaintext never.
32
+ */
33
+ /** A Resource to synchronize: its Data Profile applier (and optional checkpoints). */
34
+ /**
35
+ * How a Data Profile reads and writes Snapshots (§29, §66 step 3): its §13
36
+ * codec, loading a received Snapshot's state, and the state to publish
37
+ * (for Shared Objects: snapshotCodec(), loadSnapshot(), snapshotState()).
38
+ */
39
+ export interface SnapshotBinding<T> {
40
+ readonly codec: DataProfileCodec<T>;
41
+ load(value: T): void;
42
+ current(): T;
43
+ }
44
+ /** A Resource to synchronize: its Data Profile applier (and optional checkpoints and Snapshots). */
45
+ export interface ResourceBinding {
46
+ readonly resourceId: ResourceId;
47
+ readonly applier: DataUnitApplier;
48
+ readonly checkpointer?: ProfileCheckpointer;
49
+ /** Load an offered Snapshot instead of replaying every unit, and allow publishing. */
50
+ readonly snapshot?: SnapshotBinding<unknown>;
51
+ }
52
+ /** When to try connecting again after the `attempt`-th failure (1, 2, …): a delay in ms, or null to stop. */
53
+ export type ReconnectPolicy = (attempt: number) => number | null;
54
+ /** 1 s, doubling, at most 60 s, forever. */
55
+ export declare const defaultReconnect: ReconnectPolicy;
56
+ export interface SyncClientOptions {
57
+ readonly url: string;
58
+ /** The session and writing Principal. */
59
+ readonly signer: Signer;
60
+ /** Its X25519 key pair, to open Key Packages addressed to it. */
61
+ readonly agreement: AgreementKeyPair;
62
+ readonly storage: LfcpStorage;
63
+ readonly secrets: SecretStore;
64
+ readonly outbound: OutboundQueue;
65
+ /** The caller's clock, in milliseconds. */
66
+ readonly now: () => number;
67
+ readonly webSocket?: WebSocketFactory;
68
+ readonly reconnect?: ReconnectPolicy;
69
+ /** How often a LIVE Resource exchanges DATA_HAVE (§69); default 30 s. */
70
+ readonly antiEntropyMs?: number;
71
+ /**
72
+ * How long a request waits for its answer on a live connection before
73
+ * the step it belongs to is issued again (a lost request or reply, §70
74
+ * at-least-once): RESOURCE_OPEN, the Control round, KEY_PACKAGE_GET, the
75
+ * data round. A lost SNAPSHOT_GET falls back to the data round. Default 15 s.
76
+ */
77
+ readonly requestTimeoutMs?: number;
78
+ /** An opaque hosting credential for AUTH (§36): server policy only. */
79
+ readonly credential?: Uint8Array;
80
+ readonly dataProfiles?: readonly string[];
81
+ /**
82
+ * Whether to publish a Snapshot of a LIVE Resource now, asked on every
83
+ * tick with the number of units merged since the last one; no automatic
84
+ * schedule otherwise (publishSnapshot can also be called directly).
85
+ */
86
+ readonly snapshotPolicy?: (resourceId: ResourceId, unitsSinceLast: number) => boolean;
87
+ }
88
+ /** What the session reports to the application. */
89
+ export type SyncEvent = {
90
+ readonly type: "connection";
91
+ readonly state: ClientConnectionState;
92
+ readonly reason?: string;
93
+ } | {
94
+ readonly type: "resource-state";
95
+ readonly resourceId: ResourceId;
96
+ readonly state: ResourcePhase;
97
+ }
98
+ /** A received unit's outcome (merged, held, quarantined, equivocation, …). */
99
+ | {
100
+ readonly type: "unit";
101
+ readonly resourceId: ResourceId;
102
+ readonly outcome: ApplyOutcome;
103
+ }
104
+ /** Merged units a new Key Epoch excluded, and the objects that changed (G-EP7). */
105
+ | {
106
+ readonly type: "epoch-reconciled";
107
+ readonly resourceId: ResourceId;
108
+ readonly applied: EpochReconciliation;
109
+ readonly outbound: readonly StaleOutboundUnit[];
110
+ } | {
111
+ readonly type: "control-conflict";
112
+ readonly resourceId: ResourceId;
113
+ readonly heads: readonly ControlRecordId[];
114
+ } | {
115
+ readonly type: "key-blocked";
116
+ readonly resourceId: ResourceId;
117
+ readonly epochs: readonly DataEpoch[];
118
+ }
119
+ /** A Snapshot was loaded (§66 step 3); only units beyond its frontier are fetched. */
120
+ | {
121
+ readonly type: "snapshot-loaded";
122
+ readonly resourceId: ResourceId;
123
+ readonly snapshotId: Hash32;
124
+ readonly frontier: HaveVector;
125
+ } | {
126
+ readonly type: "snapshot-published";
127
+ readonly resourceId: ResourceId;
128
+ readonly snapshotId: Hash32;
129
+ }
130
+ /** Stored accepted units applied again to a profile state that lacked them (restart). */
131
+ | {
132
+ readonly type: "replayed";
133
+ readonly resourceId: ResourceId;
134
+ readonly replayed: readonly DataUnitId[];
135
+ readonly skipped: readonly {
136
+ readonly unitId: DataUnitId;
137
+ readonly reason: string;
138
+ }[];
139
+ /** Units quarantined because they crashed the profile engine twice by themselves. */
140
+ readonly crashed: readonly DataUnitId[];
141
+ } | {
142
+ readonly type: "ack";
143
+ readonly outcome: AckOutcome;
144
+ }
145
+ /** A NACK of an outbound object: stale, equivocation alarm, rejected, repropose, … */
146
+ | {
147
+ readonly type: "nack";
148
+ readonly outcome: NackOutcome;
149
+ }
150
+ /** A refusal or failure that concerns a Resource or the session (codes and reasons, never payloads). */
151
+ | {
152
+ readonly type: "error";
153
+ readonly resourceId?: ResourceId;
154
+ readonly code: string;
155
+ readonly message: string;
156
+ };
157
+ export declare class SyncClient {
158
+ #private;
159
+ constructor(options: SyncClientOptions);
160
+ /** Subscribes to session events; returns the unsubscribe function. */
161
+ on(listener: (event: SyncEvent) => void): () => void;
162
+ /** Resolves once every message received so far has been handled (tests and shutdown). */
163
+ idle(): Promise<void>;
164
+ get connectionState(): ClientConnectionState;
165
+ resourceState(resource: ResourceId): ResourcePhase;
166
+ /** Connects, and reconnects after losses (per the ReconnectPolicy) until stop(). */
167
+ start(): void;
168
+ /** Closes the connection and stops reconnecting. Local state stays usable. */
169
+ stop(): Promise<void>;
170
+ /** Registers a Resource and opens it now (or when the session is READY). */
171
+ open(binding: ResourceBinding): void;
172
+ /** RESOURCE_CLOSE (§43): no more pushes; local state stays usable. */
173
+ close(resource: ResourceId): void;
174
+ /** RESOURCE_HOST (§39): asks the server to host a Resource from its exact Genesis bytes; resolves with the durability applied. */
175
+ host(genesis: Uint8Array, credential?: Uint8Array): Promise<bigint>;
176
+ /** Sends what the outbound queue has due for every Resource past KEY_SYNC (e.g. after a local write). */
177
+ flush(): void;
178
+ /**
179
+ * The periodic work, on the caller's clock: heartbeat (§38), reconnect,
180
+ * anti-entropy DATA_HAVE for LIVE Resources (§69), Key Package retries,
181
+ * due outbound retries and debounced checkpoints.
182
+ */
183
+ tick(now: number): void;
184
+ /**
185
+ * Publishes a Snapshot of the Resource's current state (§29): its frontier
186
+ * is every merged unit and every stored Snapshot's frontier, so a client
187
+ * that loads it never skips content the state does not hold. Queued and
188
+ * sent like any object; returns its ID.
189
+ */
190
+ publishSnapshot(resource: ResourceId): Promise<Hash32>;
191
+ }
192
+ /**
193
+ * A thin timer driver for SyncClient: calls tick(now) every `intervalMs`
194
+ * with the given clock and timer functions (setInterval/clearInterval in
195
+ * production). Returns the stop function. The core never sets timers
196
+ * itself; this is the one explicit place that does.
197
+ */
198
+ export declare function startSyncDriver(client: SyncClient, timers: {
199
+ readonly setInterval: (fn: () => void, ms: number) => unknown;
200
+ readonly clearInterval: (handle: unknown) => void;
201
+ readonly now: () => number;
202
+ }, intervalMs?: number): () => void;
203
+ //# sourceMappingURL=sync-client.d.ts.map