keri-ts 2.2.2 → 2.2.4

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.
Files changed (55) hide show
  1. package/esm/keri/npm/src/app/bootstrap/authenticating.js +2 -2
  2. package/esm/keri/npm/src/app/bootstrap/oobiery.js +2 -2
  3. package/esm/keri/npm/src/app/delivery/forwarding.js +11 -4
  4. package/esm/keri/npm/src/app/identity/delegating.js +2 -5
  5. package/esm/keri/npm/src/app/identity/habbing.js +48 -5
  6. package/esm/keri/npm/src/app/ipex/issuance.js +11 -0
  7. package/esm/keri/npm/src/app/ipex/unblinding-workflow.js +10 -0
  8. package/esm/keri/npm/src/app/multisig/counseling.js +4 -0
  9. package/esm/keri/npm/src/app/multisig/group-members.js +22 -0
  10. package/esm/keri/npm/src/app/multisig/replace-member.js +34 -0
  11. package/esm/keri/npm/src/app/protocol/protocol-processing.js +15 -6
  12. package/esm/keri/npm/src/app/queries/witness-inquisitor.js +10 -6
  13. package/esm/keri/npm/src/app/runtime/agent-runtime.js +11 -7
  14. package/esm/keri/npm/src/app/runtime/doing.js +25 -0
  15. package/esm/keri/npm/src/db/core/lmdber.js +25 -6
  16. package/esm/keri/npm/src/db/core/memstore.js +4 -0
  17. package/esm/keri/npm/src/db/core/store-owner.js +4 -0
  18. package/esm/keri/npm/src/runtime/index.js +1 -0
  19. package/esm/keri/npm/src/version.js +2 -2
  20. package/package.json +1 -1
  21. package/types/app/bootstrap/authenticating.d.ts.map +1 -1
  22. package/types/app/bootstrap/oobiery.d.ts.map +1 -1
  23. package/types/app/delivery/forwarding.d.ts +6 -2
  24. package/types/app/delivery/forwarding.d.ts.map +1 -1
  25. package/types/app/identity/delegating.d.ts.map +1 -1
  26. package/types/app/identity/habbing.d.ts +11 -0
  27. package/types/app/identity/habbing.d.ts.map +1 -1
  28. package/types/app/ipex/issuance.d.ts +5 -0
  29. package/types/app/ipex/issuance.d.ts.map +1 -1
  30. package/types/app/ipex/unblinding-workflow.d.ts +5 -0
  31. package/types/app/ipex/unblinding-workflow.d.ts.map +1 -1
  32. package/types/app/multisig/counseling.d.ts +2 -0
  33. package/types/app/multisig/counseling.d.ts.map +1 -1
  34. package/types/app/multisig/group-members.d.ts +6 -0
  35. package/types/app/multisig/group-members.d.ts.map +1 -1
  36. package/types/app/multisig/replace-member.d.ts +30 -0
  37. package/types/app/multisig/replace-member.d.ts.map +1 -0
  38. package/types/app/protocol/protocol-processing.d.ts +6 -1
  39. package/types/app/protocol/protocol-processing.d.ts.map +1 -1
  40. package/types/app/queries/witness-inquisitor.d.ts +1 -1
  41. package/types/app/queries/witness-inquisitor.d.ts.map +1 -1
  42. package/types/app/runtime/agent-runtime.d.ts.map +1 -1
  43. package/types/app/runtime/doing.d.ts +17 -0
  44. package/types/app/runtime/doing.d.ts.map +1 -1
  45. package/types/db/core/dber.d.ts +5 -0
  46. package/types/db/core/dber.d.ts.map +1 -1
  47. package/types/db/core/lmdber.d.ts +6 -0
  48. package/types/db/core/lmdber.d.ts.map +1 -1
  49. package/types/db/core/memstore.d.ts +2 -0
  50. package/types/db/core/memstore.d.ts.map +1 -1
  51. package/types/db/core/store-owner.d.ts +2 -0
  52. package/types/db/core/store-owner.d.ts.map +1 -1
  53. package/types/runtime/index.d.ts +1 -0
  54. package/types/runtime/index.d.ts.map +1 -1
  55. package/types/version.d.ts +2 -2
@@ -1,6 +1,6 @@
1
1
  import { call, scoped } from "effection";
2
2
  import { persistResolvedContact } from "../identity/organizing.js";
3
- import { doEvery, PROTOCOL_PACING } from "../runtime/doing.js";
3
+ import { doEveryOnWrite, PROTOCOL_PACING } from "../runtime/doing.js";
4
4
  import { systemClock } from "../runtime/runtime-clock.js";
5
5
  import { isWellKnownOobiUrl, parseOobiUrl } from "./oobiery.js";
6
6
  /**
@@ -70,7 +70,7 @@ export class Authenticator {
70
70
  }
71
71
  /** Continuous auth doer for long-lived runtime hosts. */
72
72
  *authDo(clock = systemClock, pacing = PROTOCOL_PACING) {
73
- return yield* doEvery(clock, pacing.tockMs, () => this.processOnce());
73
+ return yield* doEveryOnWrite(clock, pacing, this.hby.db, () => this.processOnce());
74
74
  }
75
75
  /** Promote one eligible `woobi.` record into the fetchable `mfa.` queue. */
76
76
  promoteQueuedWellKnown() {
@@ -8,7 +8,7 @@ import { acceptReplyDecision, unverifiedReplyDecision } from "../../core/routing
8
8
  import { Schemer } from "../../core/scheming.js";
9
9
  import { persistResolvedContact } from "../identity/organizing.js";
10
10
  import { abortable } from "../runtime/abortable.js";
11
- import { doEvery, PROTOCOL_PACING } from "../runtime/doing.js";
11
+ import { doEveryOnWrite, PROTOCOL_PACING } from "../runtime/doing.js";
12
12
  import { systemClock } from "../runtime/runtime-clock.js";
13
13
  import { collectBytes } from "../transport/byte-source.js";
14
14
  import { processMessages } from "../transport/message-source.js";
@@ -207,7 +207,7 @@ export class Oobiery {
207
207
  * doer rather than root-owned queue plumbing.
208
208
  */
209
209
  *oobiDo(clock = systemClock, pacing = PROTOCOL_PACING) {
210
- return yield* doEvery(clock, pacing.tockMs, () => this.processOnce());
210
+ return yield* doEveryOnWrite(clock, pacing, this.hby.db, () => this.processOnce());
211
211
  }
212
212
  /** Select queued work, including a surviving in-flight row whose former process is gone. */
213
213
  nextQueued() {
@@ -9,7 +9,9 @@ import { mailboxTopicKey, preferredUrl } from "../mailbox/mailboxing.js";
9
9
  import { transportMailboxTopic } from "../mailbox/topic-routing.js";
10
10
  import { localGroupMember } from "../multisig/group-members.js";
11
11
  import { abortable } from "../runtime/abortable.js";
12
+ import { PROTOCOL_TOCK_MS } from "../runtime/doing.js";
12
13
  import { runtimeTurn } from "../runtime/runtime-turn.js";
14
+ import { waitOnClock } from "../runtime/workflow-contract.js";
13
15
  import { CesrHttpRefusal } from "../transport/cesr-egress.js";
14
16
  import { messengerFrom, streamMessengerFrom } from "../transport/messengering.js";
15
17
  import { requireTcpDialer } from "../transport/tcp-dialer.js";
@@ -283,11 +285,16 @@ export class Poster {
283
285
  }
284
286
  return found;
285
287
  }
286
- /** Run queued delivery independently of query response intake and producer lifetimes. */
287
- *deliverDo() {
288
+ /**
289
+ * Run queued delivery independently of query response intake and producer lifetimes, as a fixed-tock doer:
290
+ * a pass only when something is queued (an emptiness check, no store read), then one tock. A message queued
291
+ * between tocks waits at most `tockMs` before its delivery starts.
292
+ */
293
+ *deliverDo(tockMs = PROTOCOL_TOCK_MS) {
288
294
  while (!this.closed) {
289
- yield* this.process();
290
- yield* runtimeTurn();
295
+ if (!this.evts.empty)
296
+ yield* this.process();
297
+ yield* waitOnClock(this.clock, tockMs);
291
298
  }
292
299
  }
293
300
  /** Runnable and active work determine convergence; consumable observations do not. */
@@ -18,6 +18,7 @@ import { DELEGATE_MAILBOX_TOPIC, REPLAY_MAILBOX_TOPIC } from "../../core/mailbox
18
18
  import { Schemes } from "../../core/schemes.js";
19
19
  import { dgKey } from "../../db/core/keys.js";
20
20
  import { createExchangeMessage } from "../exchanges/exchange-messaging.js";
21
+ import { isLocalGroupHab, localGroupMember } from "../multisig/group-members.js";
21
22
  import { sendWitnessMessage } from "../witness/receipt-client.js";
22
23
  import { eventPayloadMessage, eventReplayMessage } from "./habbing.js";
23
24
  export const DELEGATE_REQUEST_ROUTE = "/delegate/request";
@@ -63,10 +64,6 @@ function localHabByPre(hby, pre, context) {
63
64
  }
64
65
  return hab;
65
66
  }
66
- function localGroupMember(hby, groupPre) {
67
- const record = hby.db.getHab(groupPre);
68
- return record?.mid ? hby.habs.get(record.mid) ?? null : null;
69
- }
70
67
  function workflowSaid(serder) {
71
68
  if (!serder.said) {
72
69
  throw new ValidationError("Delegated workflow event is missing a SAID.");
@@ -720,7 +717,7 @@ export class Anchorer {
720
717
  if (pinned) {
721
718
  return localHabByPre(this.hby, pinned, `Delegation workflow ${workflowId(serder)}`);
722
719
  }
723
- return localGroupMember(this.hby, hab.pre) ?? this.communicationHab(serder, hab);
720
+ return isLocalGroupHab(this.hby, hab) ? localGroupMember(this.hby, hab.pre) : this.communicationHab(serder, hab);
724
721
  }
725
722
  /**
726
723
  * Send the delegation chain before the approving event, awaiting actual Poster or witness transmission outcomes.
@@ -30,7 +30,7 @@ import { Keeper, PreSit } from "../../db/keeping.js";
30
30
  import { makeNowIso8601 } from "../../time/mod.js";
31
31
  import { oobiReplyMessages } from "../bootstrap/oobi-replies.js";
32
32
  import { loadStoredReply } from "../delivery/stored-replies.js";
33
- import { acceptedGroupMembers, latestGroupContribution } from "../multisig/group-members.js";
33
+ import { acceptedGroupMembers, isAcceptedGroupMember, latestGroupContribution } from "../multisig/group-members.js";
34
34
  import { branToSaltQb64, encodeHugeNumber, ensureKeeperCryptoReady, Manager, normalizeSaltQb64, saltySigner, } from "./keeping.js";
35
35
  /** Reserved alias for the local signatory habitat record. */
36
36
  export const SIGNER = "__signatory__";
@@ -1547,6 +1547,10 @@ export class Habery {
1547
1547
  * - this is the TypeScript analogue of `Habery.joinGroupHab()`
1548
1548
  * - it does not fabricate key state; accepted KEL state must still arrive
1549
1549
  * through `Kevery.processEvent()`
1550
+ * - KERIpy overwrites the recorded member unconditionally. Here a recorded member that is still a
1551
+ * signing or rotation member of the group's accepted key state is kept (a Habery holding two valid
1552
+ * members must not switch which one signs), and one that is in neither list is replaced by `mhab`.
1553
+ * Replacing creates no event and signs nothing; membership itself changes only by rotation.
1550
1554
  */
1551
1555
  joinGroupHab(pre, group, mhab, smids, rmids, ns) {
1552
1556
  if (!pre) {
@@ -1569,18 +1573,54 @@ export class Habery {
1569
1573
  throw new ValidationError(`Group alias ${group} is already bound to ${existingName}.`);
1570
1574
  }
1571
1575
  const existingRecord = this.db.getHab(pre);
1572
- if (existingRecord?.mid && existingRecord.mid !== mhab.pre) {
1573
- throw new ValidationError(`Group prefix ${pre} is already joined as ${existingRecord.name}.`);
1576
+ const replacing = !!existingRecord?.mid && existingRecord.mid !== mhab.pre;
1577
+ if (replacing) {
1578
+ // Membership is decided by accepted key state, never by the caller's lists or a proposal.
1579
+ if (!this.db.getKever(pre)) {
1580
+ throw new ValidationError(`Group prefix ${pre} is already joined as ${existingRecord.name}; whether member ${existingRecord.mid} `
1581
+ + `is still a member cannot be judged until the group has accepted key state.`);
1582
+ }
1583
+ if (isAcceptedGroupMember(this.db, pre, existingRecord.mid)) {
1584
+ throw new ValidationError(`Group prefix ${pre} is already joined as ${existingRecord.name} by member ${existingRecord.mid}, `
1585
+ + `which is still a member in its accepted key state.`);
1586
+ }
1587
+ if (!isAcceptedGroupMember(this.db, pre, mhab.pre)) {
1588
+ throw new ValidationError(`Local member identifier ${mhab.pre} is not a member in the accepted key state of group ${pre}.`);
1589
+ }
1574
1590
  }
1575
1591
  const hab = this.habs.get(pre) ?? new Hab(group, this.db, this.ks, this.mgr, this.cf, this.rtr, this.rvy, this.kevery, ns, pre);
1576
- this.persistGroupHabRecord(pre, group, mhab.pre, signingMembers, rotationMembers, ns);
1592
+ this.persistGroupHabRecord(pre, group, mhab.pre, signingMembers, rotationMembers, ns, existingRecord ?? undefined);
1577
1593
  this.db.groups.add(pre);
1578
1594
  if (hab.accepted) {
1579
1595
  this.markAcceptedGroupHab(pre);
1580
1596
  }
1581
1597
  this.habs.set(pre, hab);
1598
+ if (replacing && !this.readonly) {
1599
+ // The stored lists must follow accepted state, which the old member's record kept from being committed.
1600
+ this.recoverAcceptedGroupMembers(pre);
1601
+ this.retireGroupMemberState(pre, existingRecord.mid);
1602
+ }
1582
1603
  return hab;
1583
1604
  }
1605
+ /**
1606
+ * Drop the durable rows that address `group` to its former local member `old`: registry root copies
1607
+ * escrowed for `old` (the holder check now refuses them, so a retry would only fail) and the markers
1608
+ * of copies sent to `old` as a recipient. Rows keyed by the group itself stay. Live operations are
1609
+ * owned by the runtime and are not reached from here.
1610
+ */
1611
+ retireGroupMemberState(group, old) {
1612
+ for (const [keys] of [...this.db.rrpe.getTopItemIter()]) {
1613
+ const serder = this.db.exns.get([keys.join(".")]);
1614
+ const data = serder?.sad?.a;
1615
+ if (serder?.route === "/registry/root" && serder.sad?.ri === old && data?.gid === group) {
1616
+ this.db.rrpe.rem(keys);
1617
+ }
1618
+ }
1619
+ for (const [keys] of [...this.db.rrsn.getTopItemIter([group])]) {
1620
+ if (keys[2] === old)
1621
+ this.db.rrsn.rem(keys);
1622
+ }
1623
+ }
1584
1624
  /** Commit accepted group member metadata after the corresponding group event is accepted. */
1585
1625
  commitGroupHabMembers(pre, smids, rmids = smids) {
1586
1626
  const record = this.db.getHab(pre);
@@ -1774,12 +1814,15 @@ export class Habery {
1774
1814
  return sigers;
1775
1815
  }
1776
1816
  /** Persist group habitat metadata and member ordinals. */
1777
- persistGroupHabRecord(pre, group, mid, smids, rmids, ns) {
1817
+ persistGroupHabRecord(pre, group, mid, smids, rmids, ns, replaced) {
1778
1818
  this.db.pinHab(pre, new HabitatRecord({
1779
1819
  hid: pre,
1780
1820
  name: group,
1781
1821
  domain: ns,
1782
1822
  mid,
1823
+ // A rejoin or replacement keeps the rest of the record.
1824
+ sid: replaced?.sid,
1825
+ watchers: replaced?.watchers ? [...replaced.watchers] : undefined,
1783
1826
  smids: [...smids],
1784
1827
  rmids: [...rmids],
1785
1828
  }));
@@ -266,6 +266,17 @@ export class IssuanceOperations {
266
266
  this.ended.set(partyKey(op.grant, op.role), { role: op.role, state, reason });
267
267
  this.deps.changes?.push({ kind: "issuance", id: op.grant });
268
268
  }
269
+ /**
270
+ * End, as abandoned with `reason`, every live operation of local group `group` whose party member or frozen
271
+ * DH custodian is `member`. Releasing the approval drops the DH owner's held query. Returns the Grants ended.
272
+ */
273
+ endBoundToMember(group, member, reason) {
274
+ const bound = [...this.live.values()].filter(op => op.group?.party.group.pre === group
275
+ && (op.group.party.member.pre === member || op.group.party.custodian === member));
276
+ for (const op of bound)
277
+ this.end(op, "abandoned", reason);
278
+ return bound.map(op => op.grant);
279
+ }
269
280
  /** Drop the live entry and the DH approval; a missing approval means the owner already closed. */
270
281
  release(op) {
271
282
  this.live.delete(partyKey(op.grant, op.role));
@@ -287,6 +287,16 @@ export class UnblindingOperations {
287
287
  this.settled.set(unblindingKey(op.grant, op.role), { role: op.role, state, reason });
288
288
  this.deps.changes?.push({ kind: "unblinding", id: op.grant });
289
289
  }
290
+ /**
291
+ * End, as abandoned with `reason`, every live Unblind of local group `group` whose party member is `member`.
292
+ * Releasing the approval drops the owner's held query. Returns the Grants ended.
293
+ */
294
+ endBoundToMember(group, member, reason) {
295
+ const bound = [...this.live.values()].filter(op => op.hab.pre === group && op.group?.member.pre === member);
296
+ for (const op of bound)
297
+ this.end(op, "abandoned", reason);
298
+ return bound.map(op => op.grant);
299
+ }
290
300
  /** Drop the live entry and the owner's approval and frozen messages; delivered bytes stay the peer's. */
291
301
  release(op) {
292
302
  this.live.delete(unblindingKey(op.grant, op.role));
@@ -282,6 +282,10 @@ export class Counselor {
282
282
  for (const sn of removed)
283
283
  this.changes?.push({ kind: "group", id: `${pre}:${sn}` });
284
284
  }
285
+ /** Whether completed events are held in memory for forwarding, which a pass retries without any store write. */
286
+ get holdsForwards() {
287
+ return this.forwards.size > 0;
288
+ }
285
289
  /** One pass over all stages, as KERIpy `processEscrows`. */
286
290
  *processOnce() {
287
291
  // A runtime calls this every turn; with nothing counseled, three row counts are the whole cost.
@@ -128,6 +128,28 @@ export function groupMemberHasKey(db, member, key) {
128
128
  }
129
129
  return false;
130
130
  }
131
+ /**
132
+ * True when `aid` is a signing or rotation member of `group`'s latest accepted establishment event: one of its
133
+ * establishment events supplied a key in that event's key list or committed to a digest in its next list.
134
+ * Decided from accepted key state only. False when the group has no accepted state.
135
+ */
136
+ export function isAcceptedGroupMember(db, group, aid) {
137
+ const kever = db.getKever(group);
138
+ const event = kever ? db.getEvtSerder(group, kever.lastEst.d) : null;
139
+ if (!event)
140
+ return false;
141
+ const keys = new Set((event.verfers ?? []).map(verfer => verfer.qb64));
142
+ const digs = new Set((event.ndigers ?? []).map(diger => diger.qb64));
143
+ for (const [, said] of db.getKelItemIter(aid)) {
144
+ const member = db.getEvtSerder(aid, said);
145
+ if (!member?.estive)
146
+ continue;
147
+ if ((member.verfers ?? []).some(verfer => keys.has(verfer.qb64))
148
+ || (member.ndigers ?? []).some(diger => digs.has(diger.qb64)))
149
+ return true;
150
+ }
151
+ return false;
152
+ }
131
153
  /**
132
154
  * Select only a cryptographically valid local partial at an accepted group establishment.
133
155
  * The shared esigs table can hold unverified escrow candidates, so a matching
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Replace a local group's recorded member after a rotation removed it, and end the runtime's work bound to it.
3
+ *
4
+ * `Habery.joinGroupHab` re-points the record and clears the durable rows addressed to the old member. The
5
+ * runtime owns the live operations, so this is the one call that also ends them: an issuance whose party
6
+ * member or frozen DH custodian is the old member, and an Unblind holding it. Each ends through its owner's
7
+ * own end path, which releases the approval and drops the held `heldq`/`heldu` rows. Group IPEX messages and
8
+ * presentations keep no member binding (their contributions are already signed and the member is read live),
9
+ * so they are left to run. Nothing is signed and no event is made.
10
+ */
11
+ import { ValidationError } from "../../core/errors.js";
12
+ /**
13
+ * Replace the local member of `input.group`, then end live operations bound to the previous one.
14
+ *
15
+ * @throws ValidationError when `joinGroupHab` refuses: the group has no accepted state, the previous member is
16
+ * still a member of it, or the new one is not.
17
+ */
18
+ export function replaceGroupMember(runtime, input) {
19
+ const { hby } = runtime, { group, member } = input;
20
+ const record = hby.db.getHab(group), alias = record?.name;
21
+ if (!record?.mid || !alias)
22
+ throw new ValidationError(`Group ${group} is not a local group habitat.`);
23
+ const previous = record.mid;
24
+ if (previous === member.pre)
25
+ throw new ValidationError(`Member ${previous} is already the local member of ${group}.`);
26
+ hby.joinGroupHab(group, alias, member, input.smids, input.rmids ?? input.smids, record.domain);
27
+ const reason = `Local member ${previous} of group ${group} was replaced by ${member.pre}`;
28
+ const ops = runtime.operations;
29
+ return {
30
+ previous,
31
+ issuance: ops?.issuance.endBoundToMember(group, previous, reason) ?? [],
32
+ unblinding: ops?.unblinding.endBoundToMember(group, previous, reason) ?? [],
33
+ };
34
+ }
@@ -6,8 +6,8 @@ import { consoleLogger } from "../../core/logger.js";
6
6
  import { BasicReplyRouteHandler, Revery } from "../../core/routing.js";
7
7
  import { Exchanger } from "../exchanges/exchanging.js";
8
8
  import { QueryReplies } from "../queries/query-replies.js";
9
+ import { doEveryOnWrite, PROTOCOL_PACING } from "../runtime/doing.js";
9
10
  import { systemClock } from "../runtime/runtime-clock.js";
10
- import { waitOnClock } from "../runtime/workflow-contract.js";
11
11
  import { dispatchEnvelope, envelopeFromFrame, queryEnvelopeFromDispatch, } from "./parsering.js";
12
12
  /**
13
13
  * Wait after every escrow pass, as KERIpy's Doist cycle (`tock` 0.03125 s, app/directing.py) paces its `escrowDo`,
@@ -260,12 +260,21 @@ export class ProtocolProcessor {
260
260
  * KERIpy correspondence:
261
261
  * - mirrors the dedicated `escrowDo` loop rather than folding escrow
262
262
  * processing into parser ingress
263
+ *
264
+ * A pass is a function of the store and of time, so this is a {@link doEveryOnWrite} doer: a pass on tock 0,
265
+ * after any store write, and every `pacing.idleTocks` tocks for what only time releases (timeouts, KRAM
266
+ * windows). Data-driven work waits at most one tock.
263
267
  */
264
- *escrowDo(clock = systemClock) {
265
- while (true) {
266
- yield* this.processEscrowsOnce();
267
- yield* waitOnClock(clock, ESCROW_TOCK_MS);
268
- }
268
+ *escrowDo(clock = systemClock, pacing = PROTOCOL_PACING) {
269
+ // Escrows live in the KEL store and, for TEL and registry work, in the registry stores; a write to any of them
270
+ // runs the next tock's pass.
271
+ const stores = [this.kvy.db, this.tvy?.reger, this.registry?.store.baser].filter((store) => !!store);
272
+ const written = {
273
+ get writeActivity() {
274
+ return stores.reduce((sum, store) => sum + store.writeActivity, 0);
275
+ },
276
+ };
277
+ return yield* doEveryOnWrite(clock, pacing, written, () => this.processEscrowsOnce());
269
278
  }
270
279
  }
271
280
  /**
@@ -16,7 +16,8 @@ import { consoleLogger } from "../../core/logger.js";
16
16
  import { Roles } from "../../core/roles.js";
17
17
  import { Schemes } from "../../core/schemes.js";
18
18
  import { introduce } from "../delivery/introducing.js";
19
- import { runtimeTurn } from "../runtime/runtime-turn.js";
19
+ import { PROTOCOL_TOCK_MS } from "../runtime/doing.js";
20
+ import { waitOnClock } from "../runtime/workflow-contract.js";
20
21
  import { CesrHttpRefusal } from "../transport/cesr-egress.js";
21
22
  import { HTTPMessenger } from "../transport/http-messenger.js";
22
23
  import { requireTcpDialer } from "../transport/tcp-dialer.js";
@@ -339,12 +340,15 @@ export class WitnessInquisitor {
339
340
  * preparation/cleanup faults terminate the worker. Caller cancellation joins
340
341
  * the current attempt; it does not close the borrowed state or HTTP service.
341
342
  */
342
- *deliverDo() {
343
+ *deliverDo(tockMs = PROTOCOL_TOCK_MS) {
343
344
  while (true) {
344
- const failure = yield* this.deliveryPass();
345
- if (failure)
346
- consoleLogger.warn("Query delivery failed", failure);
347
- yield* runtimeTurn();
345
+ // Fixed tock: a pass only when a request is queued; a request waits at most `tockMs` to start.
346
+ if (!this.requests.empty) {
347
+ const failure = yield* this.deliveryPass();
348
+ if (failure)
349
+ consoleLogger.warn("Query delivery failed", failure);
350
+ }
351
+ yield* waitOnClock(this.clock, tockMs);
348
352
  }
349
353
  }
350
354
  /**
@@ -44,7 +44,7 @@ import { QueryReplies } from "../queries/query-replies.js";
44
44
  import { WitnessInquisitor } from "../queries/witness-inquisitor.js";
45
45
  import { Receiptor } from "../witness/receipting.js";
46
46
  import { cueDo } from "./cue-runtime.js";
47
- import { doEvery, PROTOCOL_PACING } from "./doing.js";
47
+ import { doEveryOnWrite, PROTOCOL_PACING } from "./doing.js";
48
48
  import { IpexAuxiliaries } from "./ipex-auxiliaries.js";
49
49
  import { composeOwners, operationOwnersDo, OWNER_PACING, } from "./operation-owners.js";
50
50
  import { SignerConditions } from "./signer-conditions.js";
@@ -632,15 +632,19 @@ export function* runAgentRuntime(runtime, options = {}) {
632
632
  const alive = (loop, make) => spawn(() => surviving(runtime, loop, make));
633
633
  const stopHoldWatch = runtime.hby.mgr.onHold(() => runtime.signerConditions.held());
634
634
  const tasks = [
635
- yield* alive("querying", () => runtime.querying.deliverDo()),
636
- yield* alive("postman", () => runtime.postman.deliverDo()),
635
+ yield* alive("querying", () => runtime.querying.deliverDo(pacing.tockMs)),
636
+ yield* alive("postman", () => runtime.postman.deliverDo(pacing.tockMs)),
637
637
  ...(runtime.vdr && telReceiptor
638
638
  ? [
639
- yield* alive("tel-publication", () => doEvery(runtime.clock, pacing.tockMs, () => publishPendingTel(runtime.hby, runtime.vdr.publication, telReceiptor, runtime.http, runtime.clock))),
639
+ yield* alive("tel-publication", () => doEveryOnWrite(runtime.clock, pacing, {
640
+ get writeActivity() {
641
+ return runtime.hby.db.writeActivity + runtime.vdr.publication.reger.writeActivity;
642
+ },
643
+ }, () => publishPendingTel(runtime.hby, runtime.vdr.publication, telReceiptor, runtime.http, runtime.clock))),
640
644
  ]
641
645
  : []),
642
646
  yield* alive("cues", () => cueDo(runtime, options, pacing)),
643
- yield* alive("escrows", () => runtime.protocol.escrowDo(runtime.clock)),
647
+ yield* alive("escrows", () => runtime.protocol.escrowDo(runtime.clock, pacing)),
644
648
  yield* alive("oobi", () => runtime.oobiery.oobiDo(runtime.clock, pacing)),
645
649
  yield* alive("authentication", () => runtime.authenticator.authDo(runtime.clock, pacing)),
646
650
  ...(runtime.signaler
@@ -655,8 +659,8 @@ export function* runAgentRuntime(runtime, options = {}) {
655
659
  }),
656
660
  ]
657
661
  : []),
658
- yield* alive("delegation", () => doEvery(runtime.clock, pacing.tockMs, () => runtime.delegating.processAllOnce())),
659
- yield* alive("counselor", () => doEvery(runtime.clock, pacing.tockMs, () => runtime.counselor.processOnce())),
662
+ yield* alive("delegation", () => doEveryOnWrite(runtime.clock, pacing, runtime.hby.db, () => runtime.delegating.processAllOnce())),
663
+ yield* alive("counselor", () => doEveryOnWrite(runtime.clock, pacing, runtime.hby.db, () => runtime.counselor.processOnce(), () => runtime.counselor.holdsForwards)),
660
664
  yield* alive("kel-publisher", () => kelPublisherDo(runtime.kelPublisher, runtime.clock, runtime.publisherPacing)),
661
665
  ...(runtime.operations && runtime.ipex
662
666
  ? [
@@ -3,10 +3,13 @@ import { waitOnClock } from "./workflow-contract.js";
3
3
  export const PROTOCOL_TOCK_MS = 31.25;
4
4
  /** Tocks between full sweeps of a loop that has one: 1920 tocks of 31.25 ms is a minute. */
5
5
  export const PROTOCOL_SWEEP_TOCKS = 1920;
6
+ /** Tocks between passes of a write-driven loop when the store did not change: 32 tocks of 31.25 ms is one second. */
7
+ export const PROTOCOL_IDLE_TOCKS = 32;
6
8
  /** The production pacing of the protocol-facing background loops. */
7
9
  export const PROTOCOL_PACING = Object.freeze({
8
10
  tockMs: PROTOCOL_TOCK_MS,
9
11
  sweepTocks: PROTOCOL_SWEEP_TOCKS,
12
+ idleTocks: PROTOCOL_IDLE_TOCKS,
10
13
  });
11
14
  /** Run `pass` forever: pass 0, wait `tockMs` on `clock`, pass 1, and so on. A thrown error ends the doer. */
12
15
  export function* doEvery(clock, tockMs, pass) {
@@ -15,3 +18,25 @@ export function* doEvery(clock, tockMs, pass) {
15
18
  yield* waitOnClock(clock, tockMs);
16
19
  }
17
20
  }
21
+ /**
22
+ * A fixed-tock doer for a loop whose work lives in store rows: pass 0, then a pass only on a tock after the
23
+ * store's write counter moved (a pass's own writes included, so it repeats until the store is quiet),
24
+ * after `busy()` says work is held in memory, or every `idleTocks`-th tock for what only time releases
25
+ * (timeouts, retry delays). A tock with nothing new costs a counter read, which touches no store, and no table scan. Rows written by
26
+ * another loop are seen on the next tock; time-driven work waits at most `idleTocks` tocks.
27
+ */
28
+ export function* doEveryOnWrite(clock, pacing, db, pass, busy = () => false) {
29
+ const idle = pacing.idleTocks ?? PROTOCOL_IDLE_TOCKS;
30
+ let seen = -1;
31
+ for (let tock = 0;; tock++) {
32
+ if (tock % idle === 0 || db.writeActivity !== seen || busy()) {
33
+ try {
34
+ yield* pass(tock);
35
+ }
36
+ finally {
37
+ seen = db.writeActivity;
38
+ }
39
+ }
40
+ yield* waitOnClock(clock, pacing.tockMs);
41
+ }
42
+ }
@@ -254,6 +254,13 @@ export class LMDBer extends BaseDBer {
254
254
  writable: true,
255
255
  value: false
256
256
  });
257
+ /** Write attempts made through this handle; a process-local counter, so reading it touches no store. */
258
+ Object.defineProperty(this, "commits", {
259
+ enumerable: true,
260
+ configurable: true,
261
+ writable: true,
262
+ value: 0
263
+ });
257
264
  this.defaults = { ...LMDBER_DEFAULTS, ...defaults };
258
265
  this.logger = options.logger ?? consoleLogger;
259
266
  this.configuredMapSize = options.mapSize;
@@ -482,7 +489,7 @@ export class LMDBer extends BaseDBer {
482
489
  setVer(val) {
483
490
  const env = this.requireEnv();
484
491
  try {
485
- env.transactionSync(() => {
492
+ this.writing(env).transactionSync(() => {
486
493
  env.putSync(b("__version__"), b(val));
487
494
  });
488
495
  }
@@ -521,6 +528,7 @@ export class LMDBer extends BaseDBer {
521
528
  }
522
529
  /** Run `fn` in one native synchronous write transaction; nesting is a child transaction. */
523
530
  transaction(fn) {
531
+ // A transaction is not itself a write: the puts and removes inside it count, so one that writes nothing does not.
524
532
  return this.requireEnv().transactionSync(fn);
525
533
  }
526
534
  /**
@@ -538,6 +546,15 @@ export class LMDBer extends BaseDBer {
538
546
  yield { key, value: copyDbBytes(entry.value) };
539
547
  }
540
548
  }
549
+ /** Count one write attempt through this handle and return the environment to write through. */
550
+ writing(env) {
551
+ this.commits++;
552
+ return env;
553
+ }
554
+ /** Counts this handle's own writes without reading the store; other processes' commits are not seen. */
555
+ get writeActivity() {
556
+ return this.commits;
557
+ }
541
558
  /** LMDB's committed transaction ID, which also counts other processes' commits. */
542
559
  get writeVersion() {
543
560
  const version = this.requireEnv().getStats().lastTxnId;
@@ -610,9 +627,11 @@ export class LMDBer extends BaseDBer {
610
627
  }
611
628
  putRow(table, key, val) {
612
629
  assertRowFits(table, key, val);
630
+ this.commits++;
613
631
  lmdbTable(table).putSync(key, val);
614
632
  }
615
633
  removeRow(table, key, val) {
634
+ this.commits++;
616
635
  return val === undefined ? lmdbTable(table).removeSync(key) : lmdbTable(table).removeSync(key, val);
617
636
  }
618
637
  /*
@@ -632,7 +651,7 @@ export class LMDBer extends BaseDBer {
632
651
  const db = lmdbTable(table);
633
652
  const env = this.requireEnv();
634
653
  try {
635
- const result = env.transactionSync(() => {
654
+ const result = this.writing(env).transactionSync(() => {
636
655
  const existing = db.get(key);
637
656
  if (existing !== null && existing !== undefined) {
638
657
  return false;
@@ -657,7 +676,7 @@ export class LMDBer extends BaseDBer {
657
676
  const db = lmdbTable(table);
658
677
  const env = this.requireEnv();
659
678
  try {
660
- env.transactionSync(() => {
679
+ this.writing(env).transactionSync(() => {
661
680
  assertRowFits(db, key, val);
662
681
  db.put(key, val);
663
682
  });
@@ -700,7 +719,7 @@ export class LMDBer extends BaseDBer {
700
719
  const db = lmdbTable(table);
701
720
  const env = this.requireEnv();
702
721
  try {
703
- const result = env.transactionSync(() => {
722
+ const result = this.writing(env).transactionSync(() => {
704
723
  if (val !== undefined) {
705
724
  // An empty key must still fail, but an over-long one can only be absent.
706
725
  if (key.length > MAX_KEY_BYTES)
@@ -809,7 +828,7 @@ export class LMDBer extends BaseDBer {
809
828
  if (keys.length === 0) {
810
829
  return false;
811
830
  }
812
- env.transactionSync(() => {
831
+ this.writing(env).transactionSync(() => {
813
832
  for (const key of keys) {
814
833
  db.remove(key);
815
834
  }
@@ -870,7 +889,7 @@ export class LMDBer extends BaseDBer {
870
889
  let result = true;
871
890
  const existing = new Set(this.getVals(db, key).map((val) => bytesHex(val)));
872
891
  const env = this.requireEnv();
873
- env.transactionSync(() => {
892
+ this.writing(env).transactionSync(() => {
874
893
  for (const val of vals) {
875
894
  const vh = bytesHex(val);
876
895
  if (existing.has(vh)) {
@@ -173,6 +173,10 @@ export class MemStore extends BaseDBer {
173
173
  this.assertOpen();
174
174
  return __classPrivateFieldGet(this, _MemStore_writeVersion, "f");
175
175
  }
176
+ /** Same count as {@link writeVersion}; in memory, so reading it is free. */
177
+ get writeActivity() {
178
+ return __classPrivateFieldGet(this, _MemStore_writeVersion, "f");
179
+ }
176
180
  /** Open or reopen the contents. A readonly open of never-created contents reports `false`. */
177
181
  *reopen(options = {}) {
178
182
  this.readonly = options.readonly ?? this.readonly;
@@ -45,6 +45,10 @@ export class StoreOwner {
45
45
  get writeVersion() {
46
46
  return this.store.writeVersion;
47
47
  }
48
+ /** Writes made through this handle, counted without a store read (see `Dber.writeActivity`). */
49
+ get writeActivity() {
50
+ return this.store.writeActivity;
51
+ }
48
52
  transaction(fn) {
49
53
  return this.store.transaction(fn);
50
54
  }
@@ -65,6 +65,7 @@ export { signGroupMessage } from "../app/multisig/multisig-messaging.js";
65
65
  export { approveAcdcV2Registry, publishAcdcV2Registry } from "../app/multisig/multisig-workflows.js";
66
66
  export { approveGroupProposal, dismissGroupProposal, findGroupProposal, listGroupProposals, } from "../app/multisig/proposals.js";
67
67
  export { proposeCredentialIssue, proposeCredentialRegistry, proposeCredentialRevocation, proposeGroupInception, proposeGroupInteraction, proposeGroupRotation, proposeRegistryEvent, settleProposedRotation, } from "../app/multisig/proposing.js";
68
+ export { replaceGroupMember, } from "../app/multisig/replace-member.js";
68
69
  export { Notifier, } from "../app/notifications/notifying.js";
69
70
  export { Signal, Signaler, } from "../app/notifications/signaling.js";
70
71
  export { SignerConditions } from "../app/runtime/signer-conditions.js";
@@ -6,9 +6,9 @@
6
6
  * `src/version.ts` files by hand; edit this template instead.
7
7
  */
8
8
  /** Package semantic version copied from the owning package manifest. */
9
- export const PACKAGE_VERSION = "2.2.2";
9
+ export const PACKAGE_VERSION = "2.2.4";
10
10
  /** Optional build metadata stamp injected by release/CI workflows. */
11
- export const BUILD_METADATA = "build.34.381a01d4673ab3a718e81ff2c7ee9b47b4c31aae";
11
+ export const BUILD_METADATA = "build.41.494decc08a02e94e55c83a49945ecaa6df7ad701";
12
12
  /** User-facing version string with build metadata appended when present. */
13
13
  export const DISPLAY_VERSION = BUILD_METADATA
14
14
  ? `${PACKAGE_VERSION}+${BUILD_METADATA}`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "keri-ts",
3
- "version": "2.2.2",
3
+ "version": "2.2.4",
4
4
  "description": "KERI TypeScript protocol and runtime library",
5
5
  "homepage": "https://github.com/kentbull/keri-ts",
6
6
  "repository": {
@@ -1 +1 @@
1
- {"version":3,"file":"authenticating.d.ts","sourceRoot":"","sources":["../../../src/app/bootstrap/authenticating.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,SAAS,EAAU,MAAM,WAAW,CAAC;AAEzD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAChD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAErD,OAAO,EAAE,KAAK,UAAU,EAA4B,MAAM,qBAAqB,CAAC;AAChF,OAAO,EAAE,KAAK,YAAY,EAAe,MAAM,6BAA6B,CAAC;AAC7E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAG9D;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC9B,EAAE,EAAE,KAAK,EACT,GAAG,EAAE,MAAM,GACV,OAAO,CAMT;AAED;;;;;;;;;;;;GAYG;AACH,qBAAa,aAAa;IACxB,+EAA+E;IAC/E,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,wEAAwE;IACxE,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAE1B;;;;;OAKG;gBACS,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU;IAKzC,sEAAsE;IACrE,WAAW,IAAI,SAAS,CAAC,IAAI,CAAC;IAe/B,yDAAyD;IACxD,MAAM,CAAC,KAAK,GAAE,YAA0B,EAAE,MAAM,GAAE,QAAQ,CAAC,UAAU,CAAmB,GAAG,SAAS,CAAC,KAAK,CAAC;IAI5G,4EAA4E;IAC5E,OAAO,CAAC,sBAAsB;IAsC9B,0DAA0D;IAC1D,OAAO,CAAC,eAAe;IAYvB;;;;;OAKG;IACH,OAAO,CAAE,kBAAkB;CAgC5B"}
1
+ {"version":3,"file":"authenticating.d.ts","sourceRoot":"","sources":["../../../src/app/bootstrap/authenticating.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,SAAS,EAAU,MAAM,WAAW,CAAC;AAEzD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAChD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAErD,OAAO,EAAE,KAAK,UAAU,EAAmC,MAAM,qBAAqB,CAAC;AACvF,OAAO,EAAE,KAAK,YAAY,EAAe,MAAM,6BAA6B,CAAC;AAC7E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAG9D;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC9B,EAAE,EAAE,KAAK,EACT,GAAG,EAAE,MAAM,GACV,OAAO,CAMT;AAED;;;;;;;;;;;;GAYG;AACH,qBAAa,aAAa;IACxB,+EAA+E;IAC/E,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,wEAAwE;IACxE,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAE1B;;;;;OAKG;gBACS,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU;IAKzC,sEAAsE;IACrE,WAAW,IAAI,SAAS,CAAC,IAAI,CAAC;IAe/B,yDAAyD;IACxD,MAAM,CAAC,KAAK,GAAE,YAA0B,EAAE,MAAM,GAAE,QAAQ,CAAC,UAAU,CAAmB,GAAG,SAAS,CAAC,KAAK,CAAC;IAI5G,4EAA4E;IAC5E,OAAO,CAAC,sBAAsB;IAsC9B,0DAA0D;IAC1D,OAAO,CAAC,eAAe;IAYvB;;;;;OAKG;IACH,OAAO,CAAE,kBAAkB;CAgC5B"}