@openlfcp/client 0.2.0-beta.6 → 0.2.0-beta.8

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.
@@ -1,3 +1,4 @@
1
+ import { LfcpError } from "@openlfcp/core";
1
2
  import { type AnyMessage, type ClientConnectionState, type ClientHandshakeConfig, type ReadySession } from "@openlfcp/wire";
2
3
  /**
3
4
  * One LFCP connection over a WebSocket (LFCP-WIRE-01 §30, §31, §34-§38,
@@ -36,6 +37,14 @@ export interface WebSocketLike {
36
37
  export type WebSocketFactory = (url: string, protocols: readonly string[]) => WebSocketLike;
37
38
  /** The platform WebSocket (globalThis.WebSocket); throws if the runtime has none. */
38
39
  export declare function platformWebSocket(): WebSocketFactory;
40
+ /**
41
+ * A send on a connection that is not READY: it closed while the caller was
42
+ * working (an ordinary network loss, not a fault of the caller's). Nothing
43
+ * was sent. Still an LfcpError UNSUPPORTED_VALUE, as before.
44
+ */
45
+ export declare class ConnectionNotReady extends LfcpError {
46
+ constructor();
47
+ }
39
48
  /** §30: the only LFCP subprotocol. */
40
49
  export declare const LFCP_SUBPROTOCOL = "lfcp-1";
41
50
  export interface ConnectionEvents {
@@ -7,6 +7,17 @@ export function platformWebSocket() {
7
7
  throw new LfcpError("UNSUPPORTED_VALUE", "this runtime has no global WebSocket; inject a factory");
8
8
  return (url, protocols) => new ctor(url, [...protocols]);
9
9
  }
10
+ /**
11
+ * A send on a connection that is not READY: it closed while the caller was
12
+ * working (an ordinary network loss, not a fault of the caller's). Nothing
13
+ * was sent. Still an LfcpError UNSUPPORTED_VALUE, as before.
14
+ */
15
+ export class ConnectionNotReady extends LfcpError {
16
+ constructor() {
17
+ super("UNSUPPORTED_VALUE", "the connection is not READY");
18
+ this.name = "ConnectionNotReady";
19
+ }
20
+ }
10
21
  /** §30: the only LFCP subprotocol. */
11
22
  export const LFCP_SUBPROTOCOL = "lfcp-1";
12
23
  const BYTES = (data) => {
@@ -147,7 +158,7 @@ export class LfcpConnection {
147
158
  /** Sends already encoded message bytes (e.g. an OutboundMessage) on a READY session. */
148
159
  sendEncoded(bytes) {
149
160
  if (this.#state !== "READY" || this.#socket === null)
150
- throw new LfcpError("UNSUPPORTED_VALUE", "the connection is not READY");
161
+ throw new ConnectionNotReady();
151
162
  if (bytes.length > this.#limit())
152
163
  throw new LfcpError("MESSAGE_TOO_LARGE", `${bytes.length} bytes exceed the session limit ${this.#limit()}`);
153
164
  this.#socket.send(bytes);
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/index.d.ts CHANGED
@@ -4,7 +4,7 @@ export { type ApplyOutcome, type DataProfileHandler, DataUnitApplier, type DataU
4
4
  export { ACCEPTANCE_DURABILITY, type BatchStatus, type BatchStatusName, batchStatus, batchStatuses, } from "./batch-status.js";
5
5
  export { type CheckpointSource, ProfileCheckpointer } from "./checkpoint.js";
6
6
  export { type ClaimJournal, pendingInvitationClaims } from "./claim-journal.js";
7
- export { type ConnectionEvents, type ConnectionOptions, LFCP_SUBPROTOCOL, LfcpConnection, platformWebSocket, type WebSocketFactory, type WebSocketLike, } from "./connection.js";
7
+ export { type ConnectionEvents, ConnectionNotReady, type ConnectionOptions, LFCP_SUBPROTOCOL, LfcpConnection, platformWebSocket, type WebSocketFactory, type WebSocketLike, } from "./connection.js";
8
8
  export { type CreateDataUnitOptions, type CreatedDataUnit, createDataUnit } from "./data-unit.js";
9
9
  export { EngineGuard, type EngineItem, isEngineTrap, type Suspicion, snapshotItem, unitItem, } from "./engine-guard.js";
10
10
  export { type AcceptedInvitation, type AcceptInvitationOptions, type AcceptInvitationProgress, type AcceptInvitationStage, abandonInvitationClaim, acceptInvitation, type CreatedInvitation, type CreateInvitationOptions, createInvitation, DEFAULT_INVITATION_ABILITIES, InvitationLink, type ResumeInvitationClaimOptions, resumeInvitationClaim, } from "./invite.js";
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@ export { DataUnitApplier, } from "./apply.js";
4
4
  export { ACCEPTANCE_DURABILITY, batchStatus, batchStatuses, } from "./batch-status.js";
5
5
  export { ProfileCheckpointer } from "./checkpoint.js";
6
6
  export { pendingInvitationClaims } from "./claim-journal.js";
7
- export { LFCP_SUBPROTOCOL, LfcpConnection, platformWebSocket, } from "./connection.js";
7
+ export { ConnectionNotReady, LFCP_SUBPROTOCOL, LfcpConnection, platformWebSocket, } from "./connection.js";
8
8
  export { createDataUnit } from "./data-unit.js";
9
9
  export { EngineGuard, isEngineTrap, snapshotItem, unitItem, } from "./engine-guard.js";
10
10
  export { abandonInvitationClaim, acceptInvitation, createInvitation, DEFAULT_INVITATION_ABILITIES, InvitationLink, resumeInvitationClaim, } from "./invite.js";
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,
@@ -6,7 +6,7 @@ import { encode } from "@openlfcp/wire/cbor";
6
6
  import { afterAccepted, afterMismatch, RECOVERY_TRANSIENT_ATTEMPTS, RECOVERY_TRANSIENT_CODES, startRecovery, } from "./access-recovery.js";
7
7
  import { ACCEPTANCE_DURABILITY, batchStatus, batchStatuses, recordEvidence, } from "./batch-status.js";
8
8
  import { readClaimJournal } from "./claim-journal.js";
9
- import { LfcpConnection } from "./connection.js";
9
+ import { ConnectionNotReady, LfcpConnection } from "./connection.js";
10
10
  import { EngineGuard, isEngineTrap, snapshotItem } from "./engine-guard.js";
11
11
  import { resourceSyncState, snapshotFrontier } from "./outbound.js";
12
12
  import { controlRecordWrites, createQueuedSnapshot, keyPackageWrites } from "./queue.js";
@@ -349,6 +349,11 @@ export class SyncClient {
349
349
  this.#queue = this.#queue
350
350
  .then(() => (this.#trapped ? undefined : fn()))
351
351
  .catch((e) => {
352
+ // The connection closed while a handler worked: nothing was sent, and
353
+ // its closing (queued here) starts over at the next READY. An
354
+ // ordinary outcome, not an INTERNAL error.
355
+ if (e instanceof ConnectionNotReady)
356
+ return;
352
357
  if (!isEngineTrap(e)) {
353
358
  this.#emit({
354
359
  type: "error",
@@ -534,10 +539,19 @@ export class SyncClient {
534
539
  // -------------------------------------------------------------------------
535
540
  // plumbing
536
541
  #request(request, message) {
542
+ try {
543
+ this.#connection.send(message);
544
+ }
545
+ catch (e) {
546
+ // Never waiting for an answer to what was not sent (the connection's
547
+ // closing already failed the requests it saw).
548
+ if (e instanceof ConnectionNotReady && request.kind === "host")
549
+ request.reject(new Error("the connection closed before RESOURCE_HOSTED: not READY"));
550
+ throw e;
551
+ }
537
552
  const key = toHex(message.messageId);
538
553
  this.#requests.set(key, request);
539
554
  this.#sentAt.set(key, this.#o.now());
540
- this.#connection.send(message);
541
555
  }
542
556
  /**
543
557
  * Requests unanswered for requestTimeoutMs on a live connection (§70: a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openlfcp/client",
3
- "version": "0.2.0-beta.6",
3
+ "version": "0.2.0-beta.8",
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.6",
46
- "@openlfcp/crypto": "^0.2.0-beta.6",
47
- "@openlfcp/storage": "^0.2.0-beta.6",
48
- "@openlfcp/wire": "^0.2.0-beta.6"
45
+ "@openlfcp/core": "^0.2.0-beta.8",
46
+ "@openlfcp/storage": "^0.2.0-beta.8",
47
+ "@openlfcp/crypto": "^0.2.0-beta.8",
48
+ "@openlfcp/wire": "^0.2.0-beta.8"
49
49
  }
50
50
  }