@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 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
- if ((seq === 1n) !== (options.previousUnitId === null))
27
- throw new LfcpError("INVALID_STRUCTURE", seq === 1n
28
- ? "the actor's first unit (sequence 1) must have a null previous unit (§26.2)"
29
- : `sequence ${seq} needs the actor's unit at sequence ${seq - 1n} as its previous unit (§26.2)`);
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
- return undefined;
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 previous = previousUnitId !== undefined
287
- ? previousUnitId
288
- : await latestAcceptedOwnUnit(storage, resource, actor);
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.7",
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.7",
46
- "@openlfcp/crypto": "^0.2.0-beta.7",
47
- "@openlfcp/storage": "^0.2.0-beta.7",
48
- "@openlfcp/wire": "^0.2.0-beta.7"
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
  }