@openlfcp/client 0.2.0 → 0.2.2
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/invite.d.ts +3 -1
- package/dist/invite.js +18 -5
- package/dist/sync-client.d.ts +5 -2
- package/dist/sync-client.js +88 -19
- package/package.json +5 -5
package/dist/invite.d.ts
CHANGED
|
@@ -77,7 +77,9 @@ export interface CreatedInvitation {
|
|
|
77
77
|
/**
|
|
78
78
|
* The inviter's side (§18, §73). The grant must validate on the stored
|
|
79
79
|
* chain and the package must pass §25.2 at the grant's head, both checked
|
|
80
|
-
* before anything is queued
|
|
80
|
+
* before anything is queued, and so is the link. The grant and the package
|
|
81
|
+
* are queued in one atomic commit, the grant first: a refused or failed
|
|
82
|
+
* commit queues neither, and a retry starts afresh.
|
|
81
83
|
* Share the link once both are ACKed: until then the grant is not on the
|
|
82
84
|
* coordinator's chain.
|
|
83
85
|
*/
|
package/dist/invite.js
CHANGED
|
@@ -4,7 +4,7 @@ import { dekSecretRef } from "@openlfcp/storage";
|
|
|
4
4
|
import { ABILITY, assembleInviteUri, createMessage, ERROR_CODE, invitationPrincipal, isGrantActive, parseControlRecord, parseInviteUri, parseKeyPackage, receiveKeyPackage, sealKeyPackage, signControlRecord, validateControlChain, verifyInvitationSecret, verifyKeyPackage, } from "@openlfcp/wire";
|
|
5
5
|
import { clearClaimJournal, journaledDeks, readClaimJournal, writeClaimJournal, } from "./claim-journal.js";
|
|
6
6
|
import { LfcpConnection } from "./connection.js";
|
|
7
|
-
import {
|
|
7
|
+
import { controlRecordWrites, keyPackageWrites } from "./queue.js";
|
|
8
8
|
import { loadControlChain, localControlChain, saveControlChain } from "./storage.js";
|
|
9
9
|
/**
|
|
10
10
|
* Link invitations and the one-time capability claim (LFCP-WIRE-01 §18,
|
|
@@ -94,7 +94,9 @@ function resolveInvitationGrant(state, linkGrantId, secret) {
|
|
|
94
94
|
/**
|
|
95
95
|
* The inviter's side (§18, §73). The grant must validate on the stored
|
|
96
96
|
* chain and the package must pass §25.2 at the grant's head, both checked
|
|
97
|
-
* before anything is queued
|
|
97
|
+
* before anything is queued, and so is the link. The grant and the package
|
|
98
|
+
* are queued in one atomic commit, the grant first: a refused or failed
|
|
99
|
+
* commit queues neither, and a retry starts afresh.
|
|
98
100
|
* Share the link once both are ACKed: until then the grant is not on the
|
|
99
101
|
* coordinator's chain.
|
|
100
102
|
*/
|
|
@@ -141,16 +143,27 @@ export async function createInvitation(options) {
|
|
|
141
143
|
const check = verifyKeyPackage(next, parseKeyPackage(sealed.bytes));
|
|
142
144
|
if (check.kind !== "authorized")
|
|
143
145
|
fail("AUTHORIZATION_FAILED", `the invitation Key Package is refused: ${check.message}`);
|
|
144
|
-
|
|
145
|
-
|
|
146
|
+
// The link is built (and its endpoints checked) before storage changes, so
|
|
147
|
+
// a refused link leaves nothing queued; then the grant and its Key Package
|
|
148
|
+
// go into ONE commit, grant first, so either both are queued, in sending
|
|
149
|
+
// order, or neither is (no grant without its package).
|
|
146
150
|
const uri = assembleInviteUri({
|
|
147
151
|
resourceId: R,
|
|
148
152
|
endpoints: options.endpoints,
|
|
149
153
|
grantId: grant.recordId,
|
|
150
154
|
secret,
|
|
151
155
|
});
|
|
156
|
+
const link = new InvitationLink(uri);
|
|
157
|
+
const keyPackageId = hash32(parseKeyPackage(sealed.bytes).signed.id);
|
|
158
|
+
const r = await options.storage.commit([
|
|
159
|
+
...controlRecordWrites(grant.bytes),
|
|
160
|
+
...keyPackageWrites(sealed.bytes),
|
|
161
|
+
]);
|
|
162
|
+
// The same refusal as the queue helpers (queue.ts): a plain Error.
|
|
163
|
+
if (!r.ok)
|
|
164
|
+
throw new Error(`the object was not queued: ${r.reason}`);
|
|
152
165
|
return Object.freeze({
|
|
153
|
-
link
|
|
166
|
+
link,
|
|
154
167
|
grantId: grant.recordId,
|
|
155
168
|
invitationPrincipal: invitee,
|
|
156
169
|
keyPackageId,
|
package/dist/sync-client.d.ts
CHANGED
|
@@ -100,7 +100,7 @@ export interface StagedOperation<T> {
|
|
|
100
100
|
writes(units: readonly {
|
|
101
101
|
readonly unitId: DataUnitId;
|
|
102
102
|
}[]): readonly StorageWrite[];
|
|
103
|
-
/** Called once the commit is durable, with the units in order. */
|
|
103
|
+
/** Called once the commit is durable, with the units in order; revert() is never called after it. */
|
|
104
104
|
committed(unitIds: readonly DataUnitId[]): void;
|
|
105
105
|
}
|
|
106
106
|
/** How a Resource's Data Profile commits local batches (§3.1). */
|
|
@@ -375,7 +375,10 @@ export declare class SyncClient {
|
|
|
375
375
|
* SDK-SECTIONS-INTEGRATION-01 §3.1: commits a batch of intents on an open
|
|
376
376
|
* Resource and returns its receipt once the batch is durable: its Data
|
|
377
377
|
* Units, outbound entries, the profile checkpoint and the receipt are
|
|
378
|
-
* stored in one transaction, then the batch is sent.
|
|
378
|
+
* stored in one transaction, then the batch is sent. Once it is stored
|
|
379
|
+
* the batch stands: a send that fails leaves it queued for the next
|
|
380
|
+
* connection, and a throwing listener is reported as an error event;
|
|
381
|
+
* neither reverts it or fails the commit (LFCP-03-034). An operation that
|
|
379
382
|
* has a receipt returns it for the same intents and writes nothing, and
|
|
380
383
|
* throws OperationIdReusedError for different ones (§3.3). Runs between
|
|
381
384
|
* received units, never during their merge.
|
package/dist/sync-client.js
CHANGED
|
@@ -103,6 +103,8 @@ export class SyncClient {
|
|
|
103
103
|
#sentAt = new Map();
|
|
104
104
|
#requestTimeoutMs;
|
|
105
105
|
#listeners = new Set();
|
|
106
|
+
/** #emit is reporting a listener's failure: a failure in that report is dropped. */
|
|
107
|
+
#reportingListenerError = false;
|
|
106
108
|
/** Re-proposal attempts per Control Record (item ID hex), bounded; cleared on ACK (repropose.ts). */
|
|
107
109
|
#reproposeAttempts = new Map();
|
|
108
110
|
#antiEntropyMs;
|
|
@@ -145,9 +147,42 @@ export class SyncClient {
|
|
|
145
147
|
this.#listeners.add(listener);
|
|
146
148
|
return () => this.#listeners.delete(listener);
|
|
147
149
|
}
|
|
150
|
+
/**
|
|
151
|
+
* LFCP-03-034: every listener gets the event, and a listener that throws
|
|
152
|
+
* never interrupts the SDK's own work (a commit, an ACK, received units,
|
|
153
|
+
* a loaded Snapshot): its failure is reported as an INTERNAL error event,
|
|
154
|
+
* and a failure while reporting one is dropped (no recursion). An engine
|
|
155
|
+
* trap is the exception and is thrown on, as before: reached from message
|
|
156
|
+
* handling (#serial) it stops the session; a call such as commit,
|
|
157
|
+
* publishSnapshot or revokeAccess rejects with it, and the session is not
|
|
158
|
+
* stopped.
|
|
159
|
+
*/
|
|
148
160
|
#emit(event) {
|
|
161
|
+
let failed;
|
|
162
|
+
let trap;
|
|
149
163
|
for (const l of this.#listeners)
|
|
150
|
-
|
|
164
|
+
try {
|
|
165
|
+
l(event);
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
if (isEngineTrap(error))
|
|
169
|
+
trap ??= { error };
|
|
170
|
+
else
|
|
171
|
+
failed ??= { error };
|
|
172
|
+
}
|
|
173
|
+
if (trap !== undefined)
|
|
174
|
+
throw trap.error;
|
|
175
|
+
if (failed === undefined || this.#reportingListenerError)
|
|
176
|
+
return;
|
|
177
|
+
this.#reportingListenerError = true;
|
|
178
|
+
try {
|
|
179
|
+
const e = failed.error;
|
|
180
|
+
const resourceId = "resourceId" in event ? event.resourceId : undefined;
|
|
181
|
+
this.#error("INTERNAL", `a listener of a ${event.type} event failed: ${e instanceof Error ? e.message : String(e)}`, resourceId);
|
|
182
|
+
}
|
|
183
|
+
finally {
|
|
184
|
+
this.#reportingListenerError = false;
|
|
185
|
+
}
|
|
151
186
|
}
|
|
152
187
|
// -------------------------------------------------------------------------
|
|
153
188
|
// Status (SDK-SECTIONS-INTEGRATION-01 §4, §5; LFCP-02-026)
|
|
@@ -1398,8 +1433,9 @@ export class SyncClient {
|
|
|
1398
1433
|
]);
|
|
1399
1434
|
if (!r.ok)
|
|
1400
1435
|
throw new Error(`the revocation was not queued: ${r.reason}`);
|
|
1401
|
-
|
|
1402
|
-
await this.#
|
|
1436
|
+
// Queued: the revocation stands whatever the report or the send does.
|
|
1437
|
+
await this.#afterStored(resource, () => this.#refreshAccess(ctx));
|
|
1438
|
+
await this.#afterStored(resource, () => this.#flush(ctx));
|
|
1403
1439
|
return Object.freeze({
|
|
1404
1440
|
kind: "queued",
|
|
1405
1441
|
revoked: plan.revoked,
|
|
@@ -1414,7 +1450,10 @@ export class SyncClient {
|
|
|
1414
1450
|
* SDK-SECTIONS-INTEGRATION-01 §3.1: commits a batch of intents on an open
|
|
1415
1451
|
* Resource and returns its receipt once the batch is durable: its Data
|
|
1416
1452
|
* Units, outbound entries, the profile checkpoint and the receipt are
|
|
1417
|
-
* stored in one transaction, then the batch is sent.
|
|
1453
|
+
* stored in one transaction, then the batch is sent. Once it is stored
|
|
1454
|
+
* the batch stands: a send that fails leaves it queued for the next
|
|
1455
|
+
* connection, and a throwing listener is reported as an error event;
|
|
1456
|
+
* neither reverts it or fails the commit (LFCP-03-034). An operation that
|
|
1418
1457
|
* has a receipt returns it for the same intents and writes nothing, and
|
|
1419
1458
|
* throws OperationIdReusedError for different ones (§3.3). Runs between
|
|
1420
1459
|
* received units, never during their merge.
|
|
@@ -1552,8 +1591,9 @@ export class SyncClient {
|
|
|
1552
1591
|
await this.#guardCreate(ctx, view, intents);
|
|
1553
1592
|
const staged = binding.stage(intents);
|
|
1554
1593
|
staged?.apply();
|
|
1594
|
+
let stored;
|
|
1555
1595
|
try {
|
|
1556
|
-
|
|
1596
|
+
stored = await commitOperation(storage, {
|
|
1557
1597
|
view,
|
|
1558
1598
|
controlHead: view.state.head,
|
|
1559
1599
|
actor: this.#o.signer,
|
|
@@ -1565,23 +1605,51 @@ export class SyncClient {
|
|
|
1565
1605
|
affectedNodeIds: staged?.affectedNodeIds ?? [],
|
|
1566
1606
|
modelRevision: staged?.modelRevision ?? "",
|
|
1567
1607
|
}, (units) => staged?.writes(units) ?? []);
|
|
1568
|
-
if (!committed)
|
|
1569
|
-
staged?.revert();
|
|
1570
|
-
else
|
|
1571
|
-
staged?.committed(receipt.unitIds);
|
|
1572
|
-
if (committed) {
|
|
1573
|
-
// Our own units grow the history a Snapshot covers, as received ones do.
|
|
1574
|
-
ctx.unitsSinceSnapshot += receipt.unitIds.length;
|
|
1575
|
-
await this.#reportBatches(R, [receipt.operationId]);
|
|
1576
|
-
this.#refreshSection(ctx);
|
|
1577
|
-
}
|
|
1578
|
-
await this.#flush(ctx);
|
|
1579
|
-
return receipt;
|
|
1580
1608
|
}
|
|
1581
1609
|
catch (e) {
|
|
1610
|
+
// Not stored: the batch did not happen.
|
|
1582
1611
|
staged?.revert();
|
|
1583
1612
|
throw e;
|
|
1584
1613
|
}
|
|
1614
|
+
const { receipt, committed } = stored;
|
|
1615
|
+
// The operation had a receipt already (a concurrent commit of it): this
|
|
1616
|
+
// staging did not happen.
|
|
1617
|
+
if (!committed)
|
|
1618
|
+
staged?.revert();
|
|
1619
|
+
// LFCP-03-034: from here the batch is durable and will be sent; nothing
|
|
1620
|
+
// below may undo it in the profile state or fail the commit. A failed
|
|
1621
|
+
// step is reported, and a failed send means the queue sends it later.
|
|
1622
|
+
else {
|
|
1623
|
+
await this.#afterStored(R, () => staged?.committed(receipt.unitIds));
|
|
1624
|
+
// Our own units grow the history a Snapshot covers, as received ones do.
|
|
1625
|
+
ctx.unitsSinceSnapshot += receipt.unitIds.length;
|
|
1626
|
+
await this.#afterStored(R, () => this.#reportBatches(R, [receipt.operationId]));
|
|
1627
|
+
await this.#afterStored(R, () => this.#refreshSection(ctx));
|
|
1628
|
+
}
|
|
1629
|
+
await this.#afterStored(R, () => this.#flush(ctx));
|
|
1630
|
+
return receipt;
|
|
1631
|
+
}
|
|
1632
|
+
/**
|
|
1633
|
+
* LFCP-03-034: a step after a local write is durable (the profile's
|
|
1634
|
+
* bookkeeping, status reports, the send). Its failure never undoes the
|
|
1635
|
+
* write or fails the call that made it: a lost connection leaves the
|
|
1636
|
+
* write in the outbound queue for the next one, and anything else (the
|
|
1637
|
+
* profile's committed() throwing, say) is reported as an error event, as
|
|
1638
|
+
* #emit reports a throwing listener. An engine trap is thrown on: the
|
|
1639
|
+
* call rejects with it.
|
|
1640
|
+
*/
|
|
1641
|
+
async #afterStored(resource, step) {
|
|
1642
|
+
try {
|
|
1643
|
+
await step();
|
|
1644
|
+
}
|
|
1645
|
+
catch (e) {
|
|
1646
|
+
if (isEngineTrap(e))
|
|
1647
|
+
throw e;
|
|
1648
|
+
if (e instanceof ConnectionNotReady)
|
|
1649
|
+
return;
|
|
1650
|
+
// #emit does not throw a listener's failure: the report cannot undo the write.
|
|
1651
|
+
this.#error("INTERNAL", `a step after a durable local write failed; the write stands (${e instanceof Error ? e.message : String(e)})`, resource);
|
|
1652
|
+
}
|
|
1585
1653
|
}
|
|
1586
1654
|
async publishSnapshot(resource) {
|
|
1587
1655
|
const ctx = this.#resources.get(toHex(resource));
|
|
@@ -1622,8 +1690,9 @@ export class SyncClient {
|
|
|
1622
1690
|
value: binding.current(),
|
|
1623
1691
|
});
|
|
1624
1692
|
ctx.unitsSinceSnapshot = 0;
|
|
1625
|
-
|
|
1626
|
-
await this.#
|
|
1693
|
+
// Queued: the Snapshot stands whatever a listener or the send does.
|
|
1694
|
+
await this.#afterStored(R, () => this.#emit({ type: "snapshot-published", resourceId: R, snapshotId: created.snapshotId }));
|
|
1695
|
+
await this.#afterStored(R, () => this.#flush(ctx));
|
|
1627
1696
|
return created.snapshotId;
|
|
1628
1697
|
}
|
|
1629
1698
|
async #dataRound(ctx) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openlfcp/client",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
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": "latest"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@openlfcp/
|
|
46
|
-
"@openlfcp/
|
|
47
|
-
"@openlfcp/
|
|
48
|
-
"@openlfcp/
|
|
45
|
+
"@openlfcp/crypto": "^0.2.2",
|
|
46
|
+
"@openlfcp/storage": "^0.2.2",
|
|
47
|
+
"@openlfcp/wire": "^0.2.2",
|
|
48
|
+
"@openlfcp/core": "^0.2.2"
|
|
49
49
|
}
|
|
50
50
|
}
|