@real-life/trust-protocol 0.6.0 → 0.7.0

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/engine.js CHANGED
@@ -42,7 +42,12 @@
42
42
  // that give-up runs first as an operation of its own on the one commit
43
43
  // path (resume's CommitOp, its timer carried to the replay's answer)
44
44
  // - outbound of an act starts at attempt 1 with nextAttemptAt =
45
- // when + ceil(backoff(1)); resume() re-emits only items that are due
45
+ // when + ceil(backoff(1)); the act names that or the document's send
46
+ // deadline, whichever is first, in nextDueAt (a repeat names its live
47
+ // documents' the same way), so an idle holder's timer reaches the
48
+ // retries, the give-up and the journal's end. An act without documents
49
+ // names no journal deadline: the next resume names the end of its entry
50
+ // (a tick does not); resume() re-emits only items that are due
46
51
  // and pauses re-emission while the last step is awaiting-transport
47
52
  // (the adapter holds the item) and never re-emits an accepted one. It
48
53
  // gives up every non-terminal item (submitted, held or accepted) at
@@ -58,7 +63,24 @@
58
63
  // due for the give-up, at their current attempt; one due for it is
59
64
  // given up in a commit of its own first (6115: never handed out again). Idempotency is bounded by the journal's retention:
60
65
  // max(P90D, retry.horizonMs), the key-retention of Delivery §7 that
61
- // bounds effects and their acks; after it the same actId is a new act
66
+ // bounds effects and their acks, counted from the act's commit and
67
+ // from the last step of each of its documents, whichever is later (a
68
+ // document not yet terminal keeps the act): the act's result stays as
69
+ // long as its documents' completion does; after it the same actId is
70
+ // a new act. The retention runs in holder time (an entry leaves at the
71
+ // tick at or after its end, `when` raised to lastWhen): a clock set
72
+ // ahead by more than the retention ends it early in real time. The
73
+ // bound is one of time, not of count: one entry per human act (its
74
+ // events and document digests, the documents themselves kept beside
75
+ // it), removed by the tick that is due. A replay
76
+ // answers from the journal alone, before any preparation: no key, no
77
+ // entropy, no new document; the time it reads at changes nothing in it
78
+ // - replay(actId, when) is the journal read without the act: in the
79
+ // queue (after the settling of an unsettled commit), on the stored
80
+ // state, committing nothing, lastWhen included; it names which of the
81
+ // act's documents a repeat at max(when, lastWhen) would hand out, and
82
+ // gives none up. unknown-act: no entry under the id (never committed,
83
+ // or past the retention)
62
84
  // - KeyProvider, Log and Access rejections inside a preparation become
63
85
  // InfraError('key-provider' | 'log' | 'access') with the cause. Known
64
86
  // limitation: a provider's argument errors (a module passing a label
@@ -659,8 +681,11 @@ class HolderEngine {
659
681
  if (!u)
660
682
  return;
661
683
  const { state } = await this.#read();
684
+ // the journal entry at that commitSeq with these very events: another
685
+ // opening's commit of the same act at the same commitSeq is not this one
662
686
  const landed = jcs(state) === u.text ||
663
- (u.actId !== undefined && Object.hasOwn(state.acts, u.actId) && state.acts[u.actId].commitSeq === u.commitSeq);
687
+ (u.actId !== undefined && Object.hasOwn(state.acts, u.actId) && state.acts[u.actId].commitSeq === u.commitSeq &&
688
+ jcs(state.acts[u.actId].events) === jcs(u.events));
664
689
  this.#unsettled = null;
665
690
  if (landed)
666
691
  this.#announce(u.commitSeq, u.events);
@@ -720,6 +745,15 @@ class HolderEngine {
720
745
  attempt--;
721
746
  continue;
722
747
  }
748
+ // the timer as the act named it: the next attempt of its live documents
749
+ // (and a give-up's own timer), so a repeat answers as the act did
750
+ const soon = (t) => { if (t !== undefined && t !== null)
751
+ replayWake = replayWake === undefined ? t : Math.min(replayWake, t); };
752
+ for (const d of live) {
753
+ const it = this.#itemOf(state, d);
754
+ soon(it.nextAttemptAt);
755
+ soon(notAfterOf(it, this.#policy.horizonMs));
756
+ }
723
757
  return { ok: true, outbound: live.map((d) => this.#outboundOf(state, d)), events: freezeDeep(events), ...(replayWake !== undefined ? { nextDueAt: replayWake } : {}) };
724
758
  }
725
759
  // 2 — prepare, bound to this snapshot
@@ -798,6 +832,20 @@ class HolderEngine {
798
832
  const effects = r.renames?.length ? renameAcks(r.state.effects, r.renames) : r.state.effects;
799
833
  const next = { ...r.state, effects, commitSeq: seq, lastWhen: at, outbox, duties, acts };
800
834
  const text = encodeState(next); // validates; an invalid next state is a programming error
835
+ // the timer for the documents this commit issues: each one's first
836
+ // retry or its send deadline, whichever comes first, beside the duty
837
+ // work the module's state opened
838
+ let full = wake;
839
+ if (r.nextDueAt === undefined && def !== this.#builtins.resume) {
840
+ const soon = (t) => { if (t !== undefined && t !== null)
841
+ full = full === undefined ? t : Math.min(full, t); };
842
+ // each document's first retry or its send deadline, whichever comes first
843
+ for (const o of outbound) {
844
+ soon(outbox[o.digest].nextAttemptAt);
845
+ soon(notAfterOf(outbox[o.digest], this.#policy.horizonMs));
846
+ }
847
+ }
848
+ const fullExtra = { ...(full !== undefined ? { nextDueAt: full } : {}), ...(r.detail !== undefined ? { detail: r.detail } : {}) };
801
849
  if (this.#check) {
802
850
  const d = (r.detail ?? {});
803
851
  const pick = (k) => (d[k] !== undefined ? { [k]: d[k] } : {});
@@ -817,7 +865,7 @@ class HolderEngine {
817
865
  continue;
818
866
  // 4 — release
819
867
  this.#announce(seq, events);
820
- return { ok: true, outbound: [...emit, ...release, ...outbound], events, ...extra };
868
+ return { ok: true, outbound: [...emit, ...release, ...outbound], events, ...fullExtra };
821
869
  }
822
870
  throw new InfraError('commit-conflict', `no commit after ${this.#policy.commitAttempts} attempts`);
823
871
  }
@@ -833,8 +881,11 @@ class HolderEngine {
833
881
  out.push(p.expiresAt);
834
882
  for (const e of Object.values(state.effects))
835
883
  out.push(e.expiresAt);
836
- for (const a of Object.values(state.acts))
837
- out.push(a.at + retention);
884
+ for (const a of Object.values(state.acts)) {
885
+ const end = actEnd(state, a, retention);
886
+ if (end !== Infinity)
887
+ out.push(end);
888
+ }
838
889
  for (const it of Object.values(state.outbox)) {
839
890
  const t = triasOf(it);
840
891
  if ((t === 'delivered' || t === 'failed') && it.history.length)
@@ -983,9 +1034,10 @@ class HolderEngine {
983
1034
  let dropped = false;
984
1035
  const keep = (rec, live) => {
985
1036
  const out = {};
1037
+ // defined, not assigned: a key such as `__proto__` (an app's actId) stays an own member
986
1038
  for (const [k, v] of Object.entries(rec)) {
987
1039
  if (live(v, k))
988
- out[k] = v;
1040
+ Object.defineProperty(out, k, { value: v, enumerable: true, writable: true, configurable: true });
989
1041
  else
990
1042
  dropped = true;
991
1043
  }
@@ -995,7 +1047,7 @@ class HolderEngine {
995
1047
  // act journal after the retention
996
1048
  const effects = keep(state.effects, (x) => x.expiresAt > when);
997
1049
  const prompts = keep(state.prompts, (x) => x.expiresAt === undefined || x.expiresAt > when);
998
- const acts = keep(state.acts, (x) => x.at + retention > when);
1050
+ const acts = keep(state.acts, (x) => actEnd(state, x, retention) > when);
999
1051
  // terminal outbox items, the retention after their last step, unless a
1000
1052
  // kept journal entry still answers with them (replay needs the item)
1001
1053
  const journaled = new Set(Object.values(acts).flatMap((a) => a.outboundDigests));
@@ -1589,6 +1641,31 @@ class HolderEngine {
1589
1641
  return Object.freeze({ state, commitSeq: state.commitSeq });
1590
1642
  });
1591
1643
  }
1644
+ replay(actId, when) {
1645
+ try {
1646
+ checkWhen(when);
1647
+ }
1648
+ catch (e) {
1649
+ return Promise.reject(e);
1650
+ }
1651
+ if (typeof actId !== 'string' || actId === '')
1652
+ return Promise.resolve(Object.freeze({ ok: false, code: 'actid-missing' }));
1653
+ return this.#enqueue(async () => {
1654
+ const { state } = await this.#read();
1655
+ if (!Object.hasOwn(state.acts, actId))
1656
+ return Object.freeze({ ok: false, code: 'unknown-act' });
1657
+ const rec = state.acts[actId];
1658
+ // as a repeat of the act would see them at max(when, lastWhen), without
1659
+ // the give-up it would commit first: a pure read
1660
+ const at = Math.max(when, state.lastWhen);
1661
+ const outbound = rec.outboundDigests.map((digest) => {
1662
+ const it = Object.hasOwn(state.outbox, digest) ? state.outbox[digest] : undefined;
1663
+ const t = it ? triasOf(it) : undefined;
1664
+ return { digest, live: it !== undefined && t !== 'delivered' && t !== 'failed' && this.#giveUp(it, at) === null };
1665
+ });
1666
+ return freezeDeep({ ok: true, result: { actId: rec.actId, commitSeq: rec.commitSeq, at: rec.at, events: structuredClone(rec.events), outbound } });
1667
+ });
1668
+ }
1592
1669
  pendingTransport(when) {
1593
1670
  try {
1594
1671
  checkWhen(when);
@@ -1684,6 +1761,25 @@ class HolderEngine {
1684
1761
  }
1685
1762
  get closed() { return this.#closed; }
1686
1763
  }
1764
+ /** When the journal entry of an act may leave (tick): the retention after
1765
+ * its commit and after the last step of each of its documents that is
1766
+ * terminal — the act's result stays as long as the completion of its
1767
+ * documents does; Infinity while one of them is not terminal yet (its
1768
+ * give-up makes it terminal, and the deadline follows then). */
1769
+ const actEnd = (state, a, retention) => {
1770
+ let end = a.at;
1771
+ for (const d of a.outboundDigests) {
1772
+ if (!Object.hasOwn(state.outbox, d))
1773
+ continue;
1774
+ const it = state.outbox[d];
1775
+ const t = triasOf(it);
1776
+ if (t !== 'delivered' && t !== 'failed')
1777
+ return Infinity;
1778
+ if (it.history.length)
1779
+ end = Math.max(end, it.history[it.history.length - 1].at);
1780
+ }
1781
+ return end + retention;
1782
+ };
1687
1783
  /** The send deadline of an outbox item: the horizon from its first
1688
1784
  * submission, or the sender lifetime its type declared if that is
1689
1785
  * earlier (#giveUp's two bounds as one instant). */