@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.
- package/LICENSE +201 -0
- package/README.md +27 -0
- package/dist/apply.d.ts +299 -0
- package/dist/apply.js +741 -0
- package/dist/checkpoint.d.ts +31 -0
- package/dist/checkpoint.js +50 -0
- package/dist/connection.d.ts +87 -0
- package/dist/connection.js +210 -0
- package/dist/data-unit.d.ts +50 -0
- package/dist/data-unit.js +39 -0
- package/dist/engine-guard.d.ts +58 -0
- package/dist/engine-guard.js +148 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +15 -0
- package/dist/invite.d.ts +154 -0
- package/dist/invite.js +391 -0
- package/dist/outbound.d.ts +227 -0
- package/dist/outbound.js +508 -0
- package/dist/queue.d.ts +38 -0
- package/dist/queue.js +116 -0
- package/dist/resource-state.d.ts +34 -0
- package/dist/resource-state.js +24 -0
- package/dist/snapshot.d.ts +42 -0
- package/dist/snapshot.js +40 -0
- package/dist/storage.d.ts +96 -0
- package/dist/storage.js +289 -0
- package/dist/sync-client.d.ts +203 -0
- package/dist/sync-client.js +1032 -0
- package/package.json +50 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { type ActorSequence, type ControlRecordId, type DataEpoch, type Hash32, type PrincipalId, type ResourceId } from "@openlfcp/core";
|
|
2
|
+
import type { LfcpStorage, OutboundBlock, OutboundItem, OutboundKind } from "@openlfcp/storage";
|
|
3
|
+
import { type ControlView, type HaveVector, type LfcpMessage, type WireErrorName } from "@openlfcp/wire";
|
|
4
|
+
/**
|
|
5
|
+
* The pending outbound queue and its sync state (LFCP-036): a
|
|
6
|
+
* transport-agnostic state machine over the stored outbound items. The
|
|
7
|
+
* transport (LFCP-039a) asks for messages, sends them, and reports ACKs,
|
|
8
|
+
* NACKs and connection loss; time comes from the caller, and nothing here
|
|
9
|
+
* sleeps or sets timers.
|
|
10
|
+
*
|
|
11
|
+
* The object and the message are different things:
|
|
12
|
+
* - an item is an immutable LFCP object (Data Unit, Control Record, Key
|
|
13
|
+
* Package, Snapshot) persisted with its exact bytes before it is ever
|
|
14
|
+
* sent (createQueuedDataUnit, queueKeyPackage, …); it leaves the queue
|
|
15
|
+
* only when an ACK names its ID, or when the application discards it
|
|
16
|
+
* after it was blocked;
|
|
17
|
+
* - a message is one transport attempt: every send wraps the SAME bytes in
|
|
18
|
+
* a NEW message with a new Message ID. Nothing here creates an LFCP
|
|
19
|
+
* object, reserves a sequence or touches plaintext, so a retry can never
|
|
20
|
+
* reuse a nonce.
|
|
21
|
+
*
|
|
22
|
+
* In-flight state is memory only: after a restart (or a lost connection)
|
|
23
|
+
* every unblocked item is due again and is resent byte for byte.
|
|
24
|
+
*/
|
|
25
|
+
/** Why an item is being retried; given to the RetryPolicy. */
|
|
26
|
+
export type RetryReason =
|
|
27
|
+
/** The connection went away after send, before an answer. */
|
|
28
|
+
"connection-lost"
|
|
29
|
+
/** A NACK with a transient or unknown code. */
|
|
30
|
+
| "transient"
|
|
31
|
+
/** An ACK of its message that did not name it, or named it below the required durability. */
|
|
32
|
+
| "not-acked"
|
|
33
|
+
/** No answer within the request timeout on a live connection (a lost request or reply). */
|
|
34
|
+
| "timeout";
|
|
35
|
+
export interface RetryPolicy {
|
|
36
|
+
/**
|
|
37
|
+
* When the item may be sent again (RFC 3339, caller's clock), or null for
|
|
38
|
+
* at once. Pure: the caller owns time; the result is stored with the item.
|
|
39
|
+
*/
|
|
40
|
+
nextAttempt(failure: {
|
|
41
|
+
readonly item: OutboundItem;
|
|
42
|
+
readonly reason: RetryReason;
|
|
43
|
+
readonly code?: WireErrorName | bigint;
|
|
44
|
+
readonly now: string;
|
|
45
|
+
}): string | null;
|
|
46
|
+
}
|
|
47
|
+
/** Exponential backoff from `baseMs`, doubling per attempt, capped at `maxMs`. */
|
|
48
|
+
export declare function exponentialBackoff(baseMs: number, maxMs: number): RetryPolicy;
|
|
49
|
+
/** One message to send now, and the queued objects it carries. */
|
|
50
|
+
export interface OutboundMessage {
|
|
51
|
+
readonly resourceId: ResourceId;
|
|
52
|
+
readonly message: LfcpMessage;
|
|
53
|
+
/** The encoded message; send exactly these bytes. */
|
|
54
|
+
readonly bytes: Uint8Array;
|
|
55
|
+
readonly itemIds: readonly Hash32[];
|
|
56
|
+
}
|
|
57
|
+
export interface AckOutcome {
|
|
58
|
+
/** Items the ACK named, now removed from the queue. */
|
|
59
|
+
readonly acked: readonly Hash32[];
|
|
60
|
+
/** Items of the acknowledged message the ACK did not name: still queued. */
|
|
61
|
+
readonly notCovered: readonly Hash32[];
|
|
62
|
+
/** The durability the ACK established: READY's level when durable, else 0 (§37, §59). */
|
|
63
|
+
readonly durability: bigint;
|
|
64
|
+
/** Named, but below the required durability: still queued. */
|
|
65
|
+
readonly belowDurability: readonly Hash32[];
|
|
66
|
+
/** False when the ACK answers no message of this session (matched by object ID only). */
|
|
67
|
+
readonly correlated: boolean;
|
|
68
|
+
}
|
|
69
|
+
/** A blocked item as surfaced to the application. */
|
|
70
|
+
export interface BlockedItem {
|
|
71
|
+
readonly itemId: Hash32;
|
|
72
|
+
readonly kind: OutboundKind;
|
|
73
|
+
readonly reason: OutboundBlock;
|
|
74
|
+
readonly detail: string | null;
|
|
75
|
+
}
|
|
76
|
+
export type NackOutcome =
|
|
77
|
+
/** A per-object refusal for a message of several objects: each is now sent alone. */
|
|
78
|
+
{
|
|
79
|
+
readonly kind: "isolating";
|
|
80
|
+
readonly code: WireErrorName;
|
|
81
|
+
readonly items: readonly Hash32[];
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The item is beyond a closed epoch's cutoff (STALE_DATA_EPOCH, G-EP5):
|
|
85
|
+
* never sent again. Applying the user's intent again is a NEW unit in
|
|
86
|
+
* the current epoch, made by the application (not here).
|
|
87
|
+
*/
|
|
88
|
+
| {
|
|
89
|
+
readonly kind: "stale";
|
|
90
|
+
readonly items: readonly BlockedItem[];
|
|
91
|
+
}
|
|
92
|
+
/** The server holds another unit for this (actor, seq): our sequence was reused. A safety alarm. */
|
|
93
|
+
| {
|
|
94
|
+
readonly kind: "equivocation-alarm";
|
|
95
|
+
readonly items: readonly BlockedItem[];
|
|
96
|
+
}
|
|
97
|
+
/** Refused for good (authority is judged at the unit's own head, so it will not change). */
|
|
98
|
+
| {
|
|
99
|
+
readonly kind: "rejected";
|
|
100
|
+
readonly code: WireErrorName;
|
|
101
|
+
readonly items: readonly BlockedItem[];
|
|
102
|
+
}
|
|
103
|
+
/** A Control Record proposed on a head that moved: build a new record on `currentHead`. */
|
|
104
|
+
| {
|
|
105
|
+
readonly kind: "repropose";
|
|
106
|
+
readonly items: readonly BlockedItem[];
|
|
107
|
+
readonly currentHead: ControlRecordId | null;
|
|
108
|
+
}
|
|
109
|
+
/** The server lacks Control records: retried after the next Control sync (controlSynced). */
|
|
110
|
+
| {
|
|
111
|
+
readonly kind: "needs-control-sync";
|
|
112
|
+
readonly items: readonly Hash32[];
|
|
113
|
+
}
|
|
114
|
+
/** Transient or unknown: retried after the RetryPolicy delay. */
|
|
115
|
+
| {
|
|
116
|
+
readonly kind: "retry";
|
|
117
|
+
readonly code: WireErrorName | bigint;
|
|
118
|
+
readonly items: readonly Hash32[];
|
|
119
|
+
}
|
|
120
|
+
/** The NACK answers no message of this session. */
|
|
121
|
+
| {
|
|
122
|
+
readonly kind: "uncorrelated";
|
|
123
|
+
readonly code: WireErrorName | bigint;
|
|
124
|
+
};
|
|
125
|
+
/** A queued unit of ours that a newly known Key Epoch puts beyond its cutoff (§88 step 7). */
|
|
126
|
+
export interface StaleOutboundUnit extends BlockedItem {
|
|
127
|
+
readonly actor: PrincipalId;
|
|
128
|
+
readonly seq: ActorSequence;
|
|
129
|
+
readonly epoch: DataEpoch;
|
|
130
|
+
}
|
|
131
|
+
export interface OutboundQueueOptions {
|
|
132
|
+
readonly storage: Pick<LfcpStorage, "outbound" | "dataUnits" | "syncState" | "commit">;
|
|
133
|
+
/** Defaults to "at once" (no delay); the caller decides when to ask again. */
|
|
134
|
+
readonly retry?: RetryPolicy;
|
|
135
|
+
/** At most this many objects per DATA_PUT / KEY_PACKAGE_PUT (default 64). */
|
|
136
|
+
readonly maxObjectsPerMessage?: number;
|
|
137
|
+
/**
|
|
138
|
+
* The minimum durability (§37 level) for which an ACK removes an item
|
|
139
|
+
* (default 0: any ACK). The level of an ACK is READY's when it says
|
|
140
|
+
* durable, else 0 (§59: never stronger than advertised).
|
|
141
|
+
*/
|
|
142
|
+
readonly minimumDurability?: bigint;
|
|
143
|
+
/** How many recently ACKed IDs the sync state keeps per Resource (default 256). */
|
|
144
|
+
readonly recentAckLimit?: number;
|
|
145
|
+
/**
|
|
146
|
+
* How long a sent message waits for its answer on a live connection
|
|
147
|
+
* before its items are due again (a lost request or reply, §70
|
|
148
|
+
* at-least-once): `baseMs`, doubling per attempt of the item, at most
|
|
149
|
+
* `maxMs`. Default 10 s to 60 s. A re-put is harmless: the server answers
|
|
150
|
+
* a repeated object with the same ACK (§47, §70).
|
|
151
|
+
*/
|
|
152
|
+
readonly requestTimeout?: {
|
|
153
|
+
readonly baseMs: number;
|
|
154
|
+
readonly maxMs: number;
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
export declare class OutboundQueue {
|
|
158
|
+
#private;
|
|
159
|
+
constructor(options: OutboundQueueOptions);
|
|
160
|
+
/** READY: the server's durability level and maximum message size for this session (§37). */
|
|
161
|
+
session(ready: {
|
|
162
|
+
readonly durability: bigint;
|
|
163
|
+
readonly maxMessageBytes: bigint;
|
|
164
|
+
}): void;
|
|
165
|
+
/**
|
|
166
|
+
* The messages to send now for `resource`: every unblocked item that is
|
|
167
|
+
* not in flight, not waiting for a Control sync and due by `now`, in §88
|
|
168
|
+
* order, batched where the message type allows. Each item's attempt is
|
|
169
|
+
* recorded durably before the messages are returned. An item too large
|
|
170
|
+
* for any message is blocked ("too-large") instead.
|
|
171
|
+
*/
|
|
172
|
+
next(resource: ResourceId, now: string): Promise<OutboundMessage[]>;
|
|
173
|
+
/**
|
|
174
|
+
* An ACK (§59): removes the queued items whose IDs it names (field 1),
|
|
175
|
+
* and records them in the Resource's sync state. Items of the
|
|
176
|
+
* acknowledged message it does not name stay queued. Idempotent: a
|
|
177
|
+
* repeated ACK, or one for items already gone, changes nothing.
|
|
178
|
+
*/
|
|
179
|
+
onAck(message: LfcpMessage<"ACK">, now: string): Promise<AckOutcome>;
|
|
180
|
+
/**
|
|
181
|
+
* A NACK (§60) of one of this session's messages. Per-object codes on a
|
|
182
|
+
* message of several objects resend each object alone, to learn which one
|
|
183
|
+
* the server meant. Then:
|
|
184
|
+
* STALE_DATA_EPOCH → blocked "stale-epoch" (G-EP5: never resent);
|
|
185
|
+
* ACTOR_EQUIVOCATION → blocked "equivocation", surfaced as an alarm;
|
|
186
|
+
* AUTHORIZATION_FAILED (and other per-object refusals) → blocked "rejected";
|
|
187
|
+
* MESSAGE_TOO_LARGE → blocked "too-large";
|
|
188
|
+
* CONTROL_HEAD_MISMATCH → blocked "repropose" with the current head;
|
|
189
|
+
* MISSING_DEPENDENCY → retried after the next controlSynced();
|
|
190
|
+
* anything else → retried after the RetryPolicy delay.
|
|
191
|
+
*/
|
|
192
|
+
onNack(message: LfcpMessage<"NACK">, now: string): Promise<NackOutcome>;
|
|
193
|
+
/**
|
|
194
|
+
* The connection is gone: every message in flight is unanswered, so its
|
|
195
|
+
* items are due again (same bytes, a new message) after the RetryPolicy
|
|
196
|
+
* delay. Returns the affected item IDs.
|
|
197
|
+
*/
|
|
198
|
+
connectionLost(now: string): Promise<Hash32[]>;
|
|
199
|
+
/** The Control Plane of `resource` was synchronized: items held for MISSING_DEPENDENCY are due again. */
|
|
200
|
+
controlSynced(resource: ResourceId): Promise<void>;
|
|
201
|
+
/**
|
|
202
|
+
* §88 step 7, G-EP5: with a newly validated Control view, our queued
|
|
203
|
+
* Data Units of a closed epoch beyond its final frontier are never sent;
|
|
204
|
+
* they are blocked "stale-epoch" and surfaced (also when in flight: a
|
|
205
|
+
* later ACK still removes them). Units within the cutoff stay queued and
|
|
206
|
+
* are sent as the same bytes.
|
|
207
|
+
*/
|
|
208
|
+
reconcileEpochs(view: ControlView): Promise<StaleOutboundUnit[]>;
|
|
209
|
+
/** Removes a blocked item once the application has dealt with it (e.g. re-proposed or re-applied). */
|
|
210
|
+
discard(itemId: Hash32): Promise<void>;
|
|
211
|
+
}
|
|
212
|
+
/** A Resource's sync state: Have-based, never a global cursor. */
|
|
213
|
+
export interface ResourceSyncState {
|
|
214
|
+
/** The accepted Data Units this client holds (§28), ours included. */
|
|
215
|
+
readonly have: HaveVector;
|
|
216
|
+
/** Queued items still to be sent or answered. */
|
|
217
|
+
readonly outstanding: readonly OutboundItem[];
|
|
218
|
+
/** Queued items that will not be sent again, until discarded. */
|
|
219
|
+
readonly blocked: readonly OutboundItem[];
|
|
220
|
+
readonly recentlyAcked: readonly Hash32[];
|
|
221
|
+
readonly ackedDurability: bigint | null;
|
|
222
|
+
}
|
|
223
|
+
/** The sync state of `resource` from storage alone (valid after a restart). */
|
|
224
|
+
export declare function resourceSyncState(storage: Pick<LfcpStorage, "outbound" | "dataUnits" | "syncState" | "snapshots">, resource: ResourceId): Promise<ResourceSyncState>;
|
|
225
|
+
/** The union of the frontiers of the Snapshots stored for `resource` (those loaded or published here). */
|
|
226
|
+
export declare function snapshotFrontier(storage: Pick<LfcpStorage, "snapshots">, resource: ResourceId): Promise<HaveVector>;
|
|
227
|
+
//# sourceMappingURL=outbound.d.ts.map
|