@openlfcp/client 0.2.0-beta.7 → 0.2.0-beta.9
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/dist/data-unit.js +6 -4
- package/dist/outbound.js +21 -2
- package/dist/storage.js +7 -3
- package/package.json +5 -5
package/dist/data-unit.js
CHANGED
|
@@ -23,10 +23,12 @@ export async function createDataUnit(options) {
|
|
|
23
23
|
const plaintext = options.profile.encode(options.value);
|
|
24
24
|
const seq = await options.sequences.reserveNext(atHead.resourceId, actor);
|
|
25
25
|
options.guard?.claim(atHead.resourceId, actor, seq);
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
// §26.2: sequence 1 names no previous unit. A later one names the writer's
|
|
27
|
+
// latest own unit still accepted, or none: after a first unit that went
|
|
28
|
+
// stale (a Key Epoch cut it off) or was abandoned, the unit at 2 has a null
|
|
29
|
+
// previous, and a receiver that accepted nothing of the actor links it.
|
|
30
|
+
if (seq === 1n && options.previousUnitId !== null)
|
|
31
|
+
throw new LfcpError("INVALID_STRUCTURE", "the actor's first unit (sequence 1) must have a null previous unit (§26.2)");
|
|
30
32
|
const sealed = sealDataUnit({
|
|
31
33
|
resourceId: atHead.resourceId,
|
|
32
34
|
dataEpoch: epoch,
|
package/dist/outbound.js
CHANGED
|
@@ -36,6 +36,8 @@ const PER_OBJECT = new Set([
|
|
|
36
36
|
const CODE_NAMES = new Map(Object.entries(ERROR_CODE).map(([name, code]) => [code, name]));
|
|
37
37
|
/** Room left in a message for the envelope and body around the objects. */
|
|
38
38
|
const OVERHEAD = 1024;
|
|
39
|
+
/** How many expired flights are remembered for a late answer. */
|
|
40
|
+
const EXPIRED_KEPT = 1024;
|
|
39
41
|
const AT_ONCE = { nextAttempt: () => null };
|
|
40
42
|
/** Data Units by actor, then actor sequence; unparsable bytes last (they are blocked when sent). */
|
|
41
43
|
function unitOrder(a, b) {
|
|
@@ -65,6 +67,16 @@ export class OutboundQueue {
|
|
|
65
67
|
#maxMessageBytes = DEFAULT_MAX_MESSAGE_BYTES;
|
|
66
68
|
/** message ID hex → what it carried */
|
|
67
69
|
#flights = new Map();
|
|
70
|
+
/**
|
|
71
|
+
* Flights that expired unanswered (#expire), by message ID, the most
|
|
72
|
+
* recent {@link EXPIRED_KEPT}: a late answer to one is still an answer to
|
|
73
|
+
* our message, correlated for §4.1's acceptance evidence. Without them, an
|
|
74
|
+
* ACK handled after the timeout sweep (a busy event loop: the sweep's
|
|
75
|
+
* timer runs before the waiting message) dequeued the items as
|
|
76
|
+
* uncorrelated, their batch was "evidence-unavailable" for good, and no
|
|
77
|
+
* later ACK could change it (the Shared Tasks soak of 0.4.0-beta.17).
|
|
78
|
+
*/
|
|
79
|
+
#expired = new Map();
|
|
68
80
|
/** item ID hex → message ID hex */
|
|
69
81
|
#inFlight = new Map();
|
|
70
82
|
/**
|
|
@@ -99,6 +111,9 @@ export class OutboundQueue {
|
|
|
99
111
|
if (flight.deadline > at)
|
|
100
112
|
continue;
|
|
101
113
|
this.#flights.delete(key);
|
|
114
|
+
this.#expired.set(key, flight);
|
|
115
|
+
if (this.#expired.size > EXPIRED_KEPT)
|
|
116
|
+
this.#expired.delete(this.#expired.keys().next().value);
|
|
102
117
|
for (const id of flight.itemIds)
|
|
103
118
|
if (this.#inFlight.get(toHex(id)) === key) {
|
|
104
119
|
this.#inFlight.delete(toHex(id));
|
|
@@ -261,8 +276,12 @@ export class OutboundQueue {
|
|
|
261
276
|
return undefined;
|
|
262
277
|
const key = toHex(messageId);
|
|
263
278
|
const flight = this.#flights.get(key);
|
|
264
|
-
if (flight === undefined)
|
|
265
|
-
|
|
279
|
+
if (flight === undefined) {
|
|
280
|
+
// A late answer to a message whose flight expired: ours, correlated.
|
|
281
|
+
const late = this.#expired.get(key);
|
|
282
|
+
this.#expired.delete(key);
|
|
283
|
+
return late;
|
|
284
|
+
}
|
|
266
285
|
this.#flights.delete(key);
|
|
267
286
|
for (const id of flight.itemIds) {
|
|
268
287
|
if (this.#inFlight.get(toHex(id)) === key)
|
package/dist/storage.js
CHANGED
|
@@ -283,9 +283,13 @@ export async function createQueuedDataUnit(storage, options, also = []) {
|
|
|
283
283
|
}
|
|
284
284
|
async function createQueuedDataUnitNow(storage, options, also, resource, actor) {
|
|
285
285
|
const { onCreated, previousUnitId, ...create } = options;
|
|
286
|
-
const
|
|
287
|
-
|
|
288
|
-
|
|
286
|
+
const latest = await latestAcceptedOwnUnit(storage, resource, actor);
|
|
287
|
+
// §26.2: a null previous only when the writer holds no own unit as
|
|
288
|
+
// accepted (before its first, or after a first that went stale); while it
|
|
289
|
+
// holds one, a receiver that accepted it would hold this unit for good.
|
|
290
|
+
if (previousUnitId === null && latest !== null)
|
|
291
|
+
throw new LfcpError("INVALID_STRUCTURE", "the writer still holds an accepted own unit: its next unit names it as previous (§26.2)");
|
|
292
|
+
const previous = previousUnitId !== undefined ? previousUnitId : latest;
|
|
289
293
|
const created = await createDataUnit({
|
|
290
294
|
...create,
|
|
291
295
|
previousUnitId: previous,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openlfcp/client",
|
|
3
|
-
"version": "0.2.0-beta.
|
|
3
|
+
"version": "0.2.0-beta.9",
|
|
4
4
|
"description": "LFCP client: session, Control Plane and Data Plane synchronization.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"openlfcp",
|
|
@@ -42,9 +42,9 @@
|
|
|
42
42
|
"tag": "beta"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@openlfcp/core": "^0.2.0-beta.
|
|
46
|
-
"@openlfcp/
|
|
47
|
-
"@openlfcp/
|
|
48
|
-
"@openlfcp/
|
|
45
|
+
"@openlfcp/core": "^0.2.0-beta.9",
|
|
46
|
+
"@openlfcp/storage": "^0.2.0-beta.9",
|
|
47
|
+
"@openlfcp/wire": "^0.2.0-beta.9",
|
|
48
|
+
"@openlfcp/crypto": "^0.2.0-beta.9"
|
|
49
49
|
}
|
|
50
50
|
}
|