@real-life/trust-protocol 0.3.2 → 0.4.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/CHANGELOG.md +32 -0
- package/README.md +187 -78
- package/dist/ack.d.ts +84 -0
- package/dist/ack.d.ts.map +1 -0
- package/dist/ack.js +114 -0
- package/dist/ack.js.map +1 -0
- package/dist/adapters/vti-mediator.d.ts +154 -0
- package/dist/adapters/vti-mediator.d.ts.map +1 -0
- package/dist/adapters/vti-mediator.js +621 -0
- package/dist/adapters/vti-mediator.js.map +1 -0
- package/dist/anchor.d.ts +226 -0
- package/dist/anchor.d.ts.map +1 -0
- package/dist/anchor.js +347 -0
- package/dist/anchor.js.map +1 -0
- package/dist/{holder.d.ts → carrier-holder.d.ts} +1 -1
- package/dist/carrier-holder.d.ts.map +1 -0
- package/dist/{holder.js → carrier-holder.js} +5 -4
- package/dist/carrier-holder.js.map +1 -0
- package/dist/carrier-identity.js +4 -4
- package/dist/carrier-identity.js.map +1 -1
- package/dist/carrier.d.ts.map +1 -1
- package/dist/carrier.js +4 -3
- package/dist/carrier.js.map +1 -1
- package/dist/ceremony.d.ts +3 -2
- package/dist/ceremony.d.ts.map +1 -1
- package/dist/ceremony.js +16 -26
- package/dist/ceremony.js.map +1 -1
- package/dist/codec.d.ts +46 -0
- package/dist/codec.d.ts.map +1 -0
- package/dist/codec.js +410 -0
- package/dist/codec.js.map +1 -0
- package/dist/continuity.d.ts +67 -0
- package/dist/continuity.d.ts.map +1 -0
- package/dist/continuity.js +110 -0
- package/dist/continuity.js.map +1 -0
- package/dist/core.d.ts +4 -0
- package/dist/core.d.ts.map +1 -1
- package/dist/core.js.map +1 -1
- package/dist/crypto.d.ts +23 -7
- package/dist/crypto.d.ts.map +1 -1
- package/dist/crypto.js +54 -20
- package/dist/crypto.js.map +1 -1
- package/dist/delivery.d.ts +31 -3
- package/dist/delivery.d.ts.map +1 -1
- package/dist/delivery.js +75 -28
- package/dist/delivery.js.map +1 -1
- package/dist/encounter/ceremony.d.ts +37 -0
- package/dist/encounter/ceremony.d.ts.map +1 -0
- package/dist/encounter/ceremony.js +662 -0
- package/dist/encounter/ceremony.js.map +1 -0
- package/dist/encounter.d.ts +91 -1
- package/dist/encounter.d.ts.map +1 -1
- package/dist/encounter.js +191 -12
- package/dist/encounter.js.map +1 -1
- package/dist/engine.d.ts +291 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +1395 -0
- package/dist/engine.js.map +1 -0
- package/dist/entropy.d.ts +28 -0
- package/dist/entropy.d.ts.map +1 -0
- package/dist/entropy.js +96 -0
- package/dist/entropy.js.map +1 -0
- package/dist/events.d.ts +177 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +2 -0
- package/dist/events.js.map +1 -0
- package/dist/fixtures.d.ts +30 -0
- package/dist/fixtures.d.ts.map +1 -0
- package/dist/fixtures.js +155 -0
- package/dist/fixtures.js.map +1 -0
- package/dist/forge.d.ts +47 -0
- package/dist/forge.d.ts.map +1 -0
- package/dist/forge.js +20 -0
- package/dist/forge.js.map +1 -0
- package/dist/group-star.d.ts +251 -0
- package/dist/group-star.d.ts.map +1 -0
- package/dist/group-star.js +348 -0
- package/dist/group-star.js.map +1 -0
- package/dist/holder-api.d.ts +137 -0
- package/dist/holder-api.d.ts.map +1 -0
- package/dist/holder-api.js +132 -0
- package/dist/holder-api.js.map +1 -0
- package/dist/holder-state-v1.d.ts +801 -0
- package/dist/holder-state-v1.d.ts.map +1 -0
- package/dist/holder-state-v1.js +2 -0
- package/dist/holder-state-v1.js.map +1 -0
- package/dist/identity-state.d.ts +43 -0
- package/dist/identity-state.d.ts.map +1 -0
- package/dist/identity-state.js +345 -0
- package/dist/identity-state.js.map +1 -0
- package/dist/identity.d.ts +24 -2
- package/dist/identity.d.ts.map +1 -1
- package/dist/identity.js +70 -11
- package/dist/identity.js.map +1 -1
- package/dist/index.d.ts +26 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +77 -29
- package/dist/index.js.map +1 -1
- package/dist/internal.d.ts +36 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/internal.js +22 -0
- package/dist/internal.js.map +1 -0
- package/dist/introduction/holder.d.ts +18 -0
- package/dist/introduction/holder.d.ts.map +1 -0
- package/dist/introduction/holder.js +757 -0
- package/dist/introduction/holder.js.map +1 -0
- package/dist/introduction.d.ts +101 -0
- package/dist/introduction.d.ts.map +1 -0
- package/dist/introduction.js +132 -0
- package/dist/introduction.js.map +1 -0
- package/dist/invariants.d.ts +43 -0
- package/dist/invariants.d.ts.map +1 -0
- package/dist/invariants.js +883 -0
- package/dist/invariants.js.map +1 -0
- package/dist/keys-seed.d.ts +7 -0
- package/dist/keys-seed.d.ts.map +1 -0
- package/dist/keys-seed.js +133 -0
- package/dist/keys-seed.js.map +1 -0
- package/dist/log-memory.d.ts +12 -0
- package/dist/log-memory.d.ts.map +1 -0
- package/dist/log-memory.js +101 -0
- package/dist/log-memory.js.map +1 -0
- package/dist/membership/holder.d.ts +35 -0
- package/dist/membership/holder.d.ts.map +1 -0
- package/dist/membership/holder.js +852 -0
- package/dist/membership/holder.js.map +1 -0
- package/dist/membership.d.ts +92 -0
- package/dist/membership.d.ts.map +1 -0
- package/dist/membership.js +157 -0
- package/dist/membership.js.map +1 -0
- package/dist/ports.d.ts +237 -0
- package/dist/ports.d.ts.map +1 -0
- package/dist/ports.js +2 -0
- package/dist/ports.js.map +1 -0
- package/dist/profile.d.ts +56 -0
- package/dist/profile.d.ts.map +1 -0
- package/dist/profile.js +193 -0
- package/dist/profile.js.map +1 -0
- package/dist/prompts.d.ts +14 -0
- package/dist/prompts.d.ts.map +1 -0
- package/dist/prompts.js +69 -0
- package/dist/prompts.js.map +1 -0
- package/dist/receive.d.ts +133 -0
- package/dist/receive.d.ts.map +1 -0
- package/dist/receive.js +298 -0
- package/dist/receive.js.map +1 -0
- package/dist/results.d.ts +158 -0
- package/dist/results.d.ts.map +1 -0
- package/dist/results.js +21 -0
- package/dist/results.js.map +1 -0
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +42 -38
- package/dist/schemas.js.map +1 -1
- package/dist/sha256.d.ts +3 -0
- package/dist/sha256.d.ts.map +1 -0
- package/dist/sha256.js +69 -0
- package/dist/sha256.js.map +1 -0
- package/dist/star.d.ts +31 -0
- package/dist/star.d.ts.map +1 -0
- package/dist/star.js +56 -0
- package/dist/star.js.map +1 -0
- package/dist/state-builder.d.ts +42 -0
- package/dist/state-builder.d.ts.map +1 -0
- package/dist/state-builder.js +100 -0
- package/dist/state-builder.js.map +1 -0
- package/dist/state.d.ts +853 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/state.js +36 -0
- package/dist/state.js.map +1 -0
- package/dist/store-contract.d.ts +11 -0
- package/dist/store-contract.d.ts.map +1 -0
- package/dist/store-contract.js +189 -0
- package/dist/store-contract.js.map +1 -0
- package/dist/store-memory.d.ts +8 -0
- package/dist/store-memory.d.ts.map +1 -0
- package/dist/store-memory.js +17 -0
- package/dist/store-memory.js.map +1 -0
- package/dist/testing.d.ts +22 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +21 -0
- package/dist/testing.js.map +1 -0
- package/dist/trust/continuity.d.ts +71 -0
- package/dist/trust/continuity.d.ts.map +1 -0
- package/dist/trust/continuity.js +1216 -0
- package/dist/trust/continuity.js.map +1 -0
- package/dist/trust/duties.d.ts +67 -0
- package/dist/trust/duties.d.ts.map +1 -0
- package/dist/trust/duties.js +179 -0
- package/dist/trust/duties.js.map +1 -0
- package/dist/trust/entries.d.ts +139 -0
- package/dist/trust/entries.d.ts.map +1 -0
- package/dist/trust/entries.js +694 -0
- package/dist/trust/entries.js.map +1 -0
- package/dist/trust/group-star.d.ts +43 -0
- package/dist/trust/group-star.d.ts.map +1 -0
- package/dist/trust/group-star.js +188 -0
- package/dist/trust/group-star.js.map +1 -0
- package/dist/trust/mapping.d.ts +56 -0
- package/dist/trust/mapping.d.ts.map +1 -0
- package/dist/trust/mapping.js +584 -0
- package/dist/trust/mapping.js.map +1 -0
- package/dist/trust/profile.d.ts +25 -0
- package/dist/trust/profile.d.ts.map +1 -0
- package/dist/trust/profile.js +249 -0
- package/dist/trust/profile.js.map +1 -0
- package/dist/trust/star.d.ts +208 -0
- package/dist/trust/star.d.ts.map +1 -0
- package/dist/trust/star.js +588 -0
- package/dist/trust/star.js.map +1 -0
- package/dist/trust/state.d.ts +83 -0
- package/dist/trust/state.d.ts.map +1 -0
- package/dist/trust/state.js +154 -0
- package/dist/trust/state.js.map +1 -0
- package/dist/util-freeze.d.ts +3 -0
- package/dist/util-freeze.d.ts.map +1 -0
- package/dist/util-freeze.js +11 -0
- package/dist/util-freeze.js.map +1 -0
- package/dist/views.d.ts +147 -0
- package/dist/views.d.ts.map +1 -0
- package/dist/views.js +268 -0
- package/dist/views.js.map +1 -0
- package/dist/visibility.d.ts +5 -5
- package/dist/visibility.d.ts.map +1 -1
- package/dist/visibility.js +22 -28
- package/dist/visibility.js.map +1 -1
- package/dist/wire.d.ts +178 -84
- package/dist/wire.d.ts.map +1 -1
- package/dist/wire.js.map +1 -1
- package/package.json +56 -10
- package/schemas/holder-state-v1.schema.json +3880 -0
- package/dist/holder.d.ts.map +0 -1
- package/dist/holder.js.map +0 -1
- package/dist/probe/acks.d.ts +0 -27
- package/dist/probe/acks.d.ts.map +0 -1
- package/dist/probe/acks.js +0 -152
- package/dist/probe/acks.js.map +0 -1
- package/dist/probe/continuity.d.ts +0 -20
- package/dist/probe/continuity.d.ts.map +0 -1
- package/dist/probe/continuity.js +0 -805
- package/dist/probe/continuity.js.map +0 -1
- package/dist/probe/deps.d.ts +0 -61
- package/dist/probe/deps.d.ts.map +0 -1
- package/dist/probe/deps.js +0 -54
- package/dist/probe/deps.js.map +0 -1
- package/dist/probe/encounter.d.ts +0 -26
- package/dist/probe/encounter.d.ts.map +0 -1
- package/dist/probe/encounter.js +0 -938
- package/dist/probe/encounter.js.map +0 -1
- package/dist/probe/introduce.d.ts +0 -62
- package/dist/probe/introduce.d.ts.map +0 -1
- package/dist/probe/introduce.js +0 -306
- package/dist/probe/introduce.js.map +0 -1
- package/dist/probe/membership.d.ts +0 -96
- package/dist/probe/membership.d.ts.map +0 -1
- package/dist/probe/membership.js +0 -467
- package/dist/probe/membership.js.map +0 -1
- package/dist/probe/trust.d.ts +0 -156
- package/dist/probe/trust.d.ts.map +0 -1
- package/dist/probe/trust.js +0 -1233
- package/dist/probe/trust.js.map +0 -1
- package/dist/probe.d.ts +0 -7
- package/dist/probe.d.ts.map +0 -1
- package/dist/probe.js +0 -42
- package/dist/probe.js.map +0 -1
|
@@ -0,0 +1,694 @@
|
|
|
1
|
+
import { CLASSES, anchorSet, classOnToward, conflicts, entryOf, entryRelationships, feeding, findContact, joins, lags, liveRel, newestHeads, newestOf, supersededIn, } from './state.js';
|
|
2
|
+
import { viewDigest } from './star.js';
|
|
3
|
+
import { dropOff, followClasses } from './duties.js';
|
|
4
|
+
import { isKnownDuty } from '../codec.js';
|
|
5
|
+
import { jcs } from '../core.js';
|
|
6
|
+
const jcsEq = (a, b) => jcs(a) === jcs(b);
|
|
7
|
+
const cmp = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
|
8
|
+
const cmpDec = (a, b) => { const x = BigInt(a), y = BigInt(b); return x < y ? -1 : x > y ? 1 : 0; };
|
|
9
|
+
const byPos = (a, b) => cmpDec(a.pos, b.pos) || cmp(a.id, b.id);
|
|
10
|
+
const unique = (xs) => [...new Set(xs)];
|
|
11
|
+
/** The trust module's entry commands. */
|
|
12
|
+
export const ENTRY_COMMANDS = Object.freeze({ merge: 'trust.merge', depart: 'trust.depart' });
|
|
13
|
+
// ── the anchor head and admission ──────────────────────────────────────
|
|
14
|
+
/**
|
|
15
|
+
* The entry's anchor head from what its relationships hold (6560): the
|
|
16
|
+
* newest of the heads its feeding relationships hold; among several
|
|
17
|
+
* (a manual merge before its split) the one it holds, else `prefer`, else
|
|
18
|
+
* the smallest. Where a relationship of it feeds another entry (2145), the
|
|
19
|
+
* head only moves forward: with no feeding head, or none beyond it, it
|
|
20
|
+
* stays. With neither, the entry has none.
|
|
21
|
+
*/
|
|
22
|
+
export function settleHead(state, c, prefer = null) {
|
|
23
|
+
const rels = feeding(state, c);
|
|
24
|
+
const all = entryRelationships(state, c);
|
|
25
|
+
const newest = newestHeads(rels);
|
|
26
|
+
if (!newest.length) {
|
|
27
|
+
if (all.length > rels.length)
|
|
28
|
+
return c;
|
|
29
|
+
return c.anchorHead === null ? c : { ...c, anchorHead: null };
|
|
30
|
+
}
|
|
31
|
+
// 2145: a chain of this entry feeds the other one; what the rest feeds
|
|
32
|
+
// never moves the head back — it stays unless a feeding head lies
|
|
33
|
+
// beyond it along the links the entry's relationships hold
|
|
34
|
+
if (all.length > rels.length && c.anchorHead !== null && newestOf([...newest, c.anchorHead], all.flatMap((r) => r.links)).includes(c.anchorHead))
|
|
35
|
+
return c;
|
|
36
|
+
const head = newest.length === 1 ? newest[0]
|
|
37
|
+
: c.anchorHead !== null && newest.includes(c.anchorHead) ? c.anchorHead
|
|
38
|
+
: prefer !== null && newest.includes(prefer) ? prefer
|
|
39
|
+
: [...newest].sort(cmp)[0];
|
|
40
|
+
return head === c.anchorHead ? c : { ...c, anchorHead: head };
|
|
41
|
+
}
|
|
42
|
+
/** 2250: pending entries join the set in position order while it has room. */
|
|
43
|
+
export function admitPending(state, bound) {
|
|
44
|
+
let admitted = BigInt(Object.values(state.contacts).filter((x) => x.status === 'admitted').length);
|
|
45
|
+
const pending = Object.values(state.contacts).filter((x) => x.status === 'pending' && x.pos !== null).sort(byPos);
|
|
46
|
+
let contacts = null;
|
|
47
|
+
for (const c of pending) {
|
|
48
|
+
if (admitted >= bound)
|
|
49
|
+
break;
|
|
50
|
+
contacts ??= { ...state.contacts };
|
|
51
|
+
contacts[c.id] = { ...c, status: 'admitted' };
|
|
52
|
+
admitted += 1n;
|
|
53
|
+
}
|
|
54
|
+
return contacts ? { ...state, contacts } : state;
|
|
55
|
+
}
|
|
56
|
+
/** Rename members of one star scope's views, re-sorted and re-digested. */
|
|
57
|
+
function renameView(view, from, to) {
|
|
58
|
+
if (!view.some((p) => from(p.member)))
|
|
59
|
+
return null;
|
|
60
|
+
return view.map((p) => (from(p.member) ? { ...p, member: to } : p)).sort((a, b) => cmp(a.member, b.member));
|
|
61
|
+
}
|
|
62
|
+
function renameBaselines(t, from, to) {
|
|
63
|
+
const o = t.starOut;
|
|
64
|
+
let changed = false;
|
|
65
|
+
let snapshot = o.snapshot;
|
|
66
|
+
let snapshotDigest = o.snapshotDigest;
|
|
67
|
+
if (snapshot) {
|
|
68
|
+
const v = renameView(snapshot, from, to);
|
|
69
|
+
if (v) {
|
|
70
|
+
snapshot = v;
|
|
71
|
+
snapshotDigest = viewDigest(v);
|
|
72
|
+
changed = true;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const inflight = {};
|
|
76
|
+
for (const [salt, f] of Object.entries(o.inflight)) {
|
|
77
|
+
const v = renameView(f.view, from, to);
|
|
78
|
+
if (v) {
|
|
79
|
+
inflight[salt] = { ...f, view: v, viewDigest: viewDigest(v) };
|
|
80
|
+
changed = true;
|
|
81
|
+
}
|
|
82
|
+
else
|
|
83
|
+
inflight[salt] = f;
|
|
84
|
+
}
|
|
85
|
+
return changed ? { ...t, starOut: { ...o, snapshot, snapshotDigest, inflight } } : t;
|
|
86
|
+
}
|
|
87
|
+
/** The tuple the record a prompt asks about was made on: a group-invite's
|
|
88
|
+
* invite by its own rkid, an introduction's offer or the target's act by
|
|
89
|
+
* its pin toward the mediator; null where none is held (an
|
|
90
|
+
* encounter-confirm names the fresh relationship's own identity). */
|
|
91
|
+
function promptTuple(state, p) {
|
|
92
|
+
if (p.kind === 'group-invite') {
|
|
93
|
+
const i = Object.values(state.joining.invites).find((x) => x.promptId === p.id);
|
|
94
|
+
return i ? { rkid: i.rkid } : null;
|
|
95
|
+
}
|
|
96
|
+
if (p.kind === 'introduction') {
|
|
97
|
+
if (Object.hasOwn(state.encounter.offers, p.id))
|
|
98
|
+
return { label: state.encounter.offers[p.id].pin };
|
|
99
|
+
const a = Object.values(state.encounter.introductions).find((x) => x.role === 'target' && x.promptId === p.id);
|
|
100
|
+
return a?.pins.mediator !== undefined ? { label: a.pins.mediator } : null;
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Every holder-local key that resolved through an identity in `ids` now
|
|
106
|
+
* resolves through `to` (2100, 2140), within `opts`: tuples' entryId,
|
|
107
|
+
* duties, the recipients of issued documents, and with `global` the
|
|
108
|
+
* matches of accepted stars and merge duties; and the contact references
|
|
109
|
+
* of the membership and introduction state and of prompts, each with the
|
|
110
|
+
* relationship of the tuple it was made on (2080): by the own rkid it
|
|
111
|
+
* arrived or went on (joining preludes and invites, groups' preludes,
|
|
112
|
+
* invitations and roster) or the own pair label toward the mediator
|
|
113
|
+
* (encounter.offers by their pin, the introducer provenance.via by
|
|
114
|
+
* viaPin, memory.vouchedBy by memory.vouchedPin), a prompt with the
|
|
115
|
+
* record it asks about; one that names no tuple follows with `global`.
|
|
116
|
+
* Contacts are the caller's: their ids
|
|
117
|
+
* and aliases are what the rewrite follows. The stored acks follow
|
|
118
|
+
* through the engine (opts.renames): only the core writes the effect
|
|
119
|
+
* cache, and their sealed envelopes stay as they are. Parts without a
|
|
120
|
+
* change keep their identity.
|
|
121
|
+
*/
|
|
122
|
+
export function rewriteIdentity(state, ids, to, opts = {}) {
|
|
123
|
+
const from = (x) => ids.includes(x);
|
|
124
|
+
const inRel = opts.rels ?? (() => true);
|
|
125
|
+
// the own rkids and pair labels of the tuples within `opts`
|
|
126
|
+
const rkids = new Set(), labels = new Set();
|
|
127
|
+
for (const [id, r] of Object.entries(state.relationships)) {
|
|
128
|
+
if (inRel(id))
|
|
129
|
+
for (const t of r.tuples) {
|
|
130
|
+
rkids.add(t.rkid);
|
|
131
|
+
labels.add(t.ownPairLabel);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const byRkid = (rkid) => (rkid === undefined ? !!opts.global : !opts.rels || rkids.has(rkid));
|
|
135
|
+
const byLabel = (label) => (typeof label !== 'string' ? !!opts.global : !opts.rels || labels.has(label));
|
|
136
|
+
const re = (x, tied) => x !== undefined && from(x) && tied;
|
|
137
|
+
let relationships = null;
|
|
138
|
+
for (const [id, r] of Object.entries(state.relationships)) {
|
|
139
|
+
const mine = inRel(id);
|
|
140
|
+
let changed = false;
|
|
141
|
+
const tuples = r.tuples.map((t) => {
|
|
142
|
+
let x = t;
|
|
143
|
+
if (mine && from(t.entryId))
|
|
144
|
+
x = { ...x, entryId: to };
|
|
145
|
+
const acc = x.starIn.accepted;
|
|
146
|
+
if (opts.global && acc && acc.matches.some(from)) {
|
|
147
|
+
x = { ...x, starIn: { ...x.starIn, accepted: { ...acc, matches: unique(acc.matches.map((m) => (from(m) ? to : m))).sort(cmp) } } };
|
|
148
|
+
}
|
|
149
|
+
if (opts.baselines)
|
|
150
|
+
x = renameBaselines(x, from, to);
|
|
151
|
+
if (x !== t)
|
|
152
|
+
changed = true;
|
|
153
|
+
return x;
|
|
154
|
+
});
|
|
155
|
+
// the introducer with the relationship toward it the act was pinned to
|
|
156
|
+
const provenance = re(r.provenance.via, byLabel(r.provenance.viaPin)) ? { ...r.provenance, via: to } : r.provenance;
|
|
157
|
+
if (changed || provenance !== r.provenance)
|
|
158
|
+
(relationships ??= { ...state.relationships })[id] = { ...r, tuples, provenance };
|
|
159
|
+
}
|
|
160
|
+
opts.renames?.push({ from: [...ids], to, rkids: opts.rels ? [...rkids].sort() : null });
|
|
161
|
+
let duties = null;
|
|
162
|
+
for (const [id, d] of Object.entries(state.duties)) {
|
|
163
|
+
if (!isKnownDuty(d))
|
|
164
|
+
continue; // a kind or version this library does not handle stays as it is
|
|
165
|
+
const x = d;
|
|
166
|
+
let nd = d;
|
|
167
|
+
if (d.kind === 'merge' && opts.global && Array.isArray(x.entryIds) && x.entryIds.some(from)) {
|
|
168
|
+
nd = { ...d, entryIds: unique(x.entryIds.map((e) => (from(e) ? to : e))).sort(cmp) };
|
|
169
|
+
}
|
|
170
|
+
else if (d.kind !== 'merge' && typeof x.contactId === 'string' && from(x.contactId) && (typeof x.relId !== 'string' || inRel(x.relId))) {
|
|
171
|
+
nd = { ...d, contactId: to };
|
|
172
|
+
}
|
|
173
|
+
if (nd !== d)
|
|
174
|
+
(duties ??= { ...state.duties })[id] = nd;
|
|
175
|
+
}
|
|
176
|
+
// membership, introduction and prompts: each contact reference with the
|
|
177
|
+
// relationship of the tuple it was made on, where it names one (2080)
|
|
178
|
+
let prompts = null;
|
|
179
|
+
for (const [id, p] of Object.entries(state.prompts)) {
|
|
180
|
+
const at = promptTuple(state, p);
|
|
181
|
+
const tied = at === null ? !!opts.global : 'rkid' in at ? byRkid(at.rkid) : byLabel(at.label);
|
|
182
|
+
if (re(p.contactId, tied))
|
|
183
|
+
(prompts ??= { ...state.prompts })[id] = { ...p, contactId: to };
|
|
184
|
+
}
|
|
185
|
+
let joining = null;
|
|
186
|
+
for (const [k, p] of Object.entries(state.joining.preludes)) {
|
|
187
|
+
if (re(p.contactId, byRkid(p.rkid)))
|
|
188
|
+
(joining ??= { ...state.joining, preludes: { ...state.joining.preludes } }).preludes[k] = { ...p, contactId: to };
|
|
189
|
+
}
|
|
190
|
+
for (const [k, i] of Object.entries(state.joining.invites)) {
|
|
191
|
+
if (re(i.contactId, byRkid(i.rkid))) {
|
|
192
|
+
joining ??= { ...state.joining, preludes: { ...state.joining.preludes } };
|
|
193
|
+
joining.invites = { ...joining.invites, [k]: { ...i, contactId: to } };
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
let groups = null;
|
|
197
|
+
for (const [gid, g] of Object.entries(state.groups)) {
|
|
198
|
+
const preludes = Object.fromEntries(Object.entries(g.preludes).map(([k, p]) => [k, re(p.contactId, byRkid(p.rkid)) ? { ...p, contactId: to } : p]));
|
|
199
|
+
const invitations = Object.fromEntries(Object.entries(g.invitations).map(([k, i]) => [k, re(i.contactId, byRkid(i.rkid)) ? { ...i, contactId: to } : i]));
|
|
200
|
+
const roster = Object.fromEntries(Object.entries(g.roster).map(([k, m]) => [k, re(m.contactId, byRkid(m.rkid)) ? { ...m, contactId: to } : m]));
|
|
201
|
+
if (jcsEq(preludes, g.preludes) && jcsEq(invitations, g.invitations) && jcsEq(roster, g.roster))
|
|
202
|
+
continue;
|
|
203
|
+
(groups ??= { ...state.groups })[gid] = { ...g, preludes, invitations, roster };
|
|
204
|
+
}
|
|
205
|
+
let encounter = null;
|
|
206
|
+
for (const [k, o] of Object.entries(state.encounter.offers)) {
|
|
207
|
+
if (re(o.mediatorContactId, byLabel(o.pin)))
|
|
208
|
+
(encounter ??= { ...state.encounter, offers: { ...state.encounter.offers } }).offers[k] = { ...o, mediatorContactId: to };
|
|
209
|
+
}
|
|
210
|
+
// the voucher mark names the mediator that vouched: it follows the
|
|
211
|
+
// relationship toward that mediator the voucher came on (memory.vouchedPin),
|
|
212
|
+
// not the entry it was filed under
|
|
213
|
+
for (const [id, r] of Object.entries(relationships ?? state.relationships)) {
|
|
214
|
+
const v = r.memory.vouchedBy;
|
|
215
|
+
if (typeof v === 'string' && re(v, byLabel(r.memory.vouchedPin)))
|
|
216
|
+
(relationships ??= { ...state.relationships })[id] = { ...r, memory: { ...r.memory, vouchedBy: to } };
|
|
217
|
+
}
|
|
218
|
+
let outbox = null;
|
|
219
|
+
for (const [d, it] of Object.entries(state.outbox)) {
|
|
220
|
+
// a document on a member key follows the tuple its contact was named through (Recipient.via)
|
|
221
|
+
if (from(it.recipient.contactId) && rkids.has(it.recipient.via ?? it.recipient.rkid))
|
|
222
|
+
(outbox ??= { ...state.outbox })[d] = { ...it, recipient: { ...it.recipient, contactId: to } };
|
|
223
|
+
}
|
|
224
|
+
if (!relationships && !duties && !prompts && !outbox && !joining && !groups && !encounter)
|
|
225
|
+
return state;
|
|
226
|
+
return {
|
|
227
|
+
...state,
|
|
228
|
+
...(relationships ? { relationships } : {}), ...(duties ? { duties } : {}), ...(prompts ? { prompts } : {}), ...(outbox ? { outbox } : {}),
|
|
229
|
+
...(joining ? { joining } : {}), ...(groups ? { groups } : {}), ...(encounter ? { encounter } : {}),
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
// ── merge (2140–2180) ──────────────────────────────────────────────────
|
|
233
|
+
/** 2160: per class `off` where any held `off`, else `on` where any held
|
|
234
|
+
* `on`, else `default`; per (group, contact) the same, every per-group
|
|
235
|
+
* `on` reset where the merged sealed group star is `off` (5450). */
|
|
236
|
+
export function mergeSettings(cs) {
|
|
237
|
+
const pick = (xs) => (xs.includes('off') ? 'off' : xs.includes('on') ? 'on' : 'default');
|
|
238
|
+
const settings = {};
|
|
239
|
+
for (const cls of CLASSES)
|
|
240
|
+
settings[cls] = pick(cs.map((c) => c.settings[cls]));
|
|
241
|
+
const groupSettings = {};
|
|
242
|
+
for (const g of unique(cs.flatMap((c) => Object.keys(c.groupSettings))).sort(cmp)) {
|
|
243
|
+
const s = pick(cs.map((c) => c.groupSettings[g] ?? 'default'));
|
|
244
|
+
if (s === 'off' || (s === 'on' && settings.sealedGroupStar !== 'off'))
|
|
245
|
+
groupSettings[g] = s;
|
|
246
|
+
}
|
|
247
|
+
return { settings, groupSettings };
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Whether entries can be one (6605, 6560): no relationship of one holds
|
|
251
|
+
* another binding (or another successor) for an anchor a relationship of
|
|
252
|
+
* another holds, and their anchor heads lie on one chain of the links
|
|
253
|
+
* their relationships hold, so the merged entry has exactly one.
|
|
254
|
+
*/
|
|
255
|
+
export function mergeable(state, cs) {
|
|
256
|
+
if (cs.length < 2 || new Set(cs.map((c) => c.id)).size !== cs.length)
|
|
257
|
+
return false;
|
|
258
|
+
const rels = cs.map((c) => entryRelationships(state, c));
|
|
259
|
+
for (let i = 0; i < rels.length; i++)
|
|
260
|
+
for (let j = i + 1; j < rels.length; j++)
|
|
261
|
+
if (conflicts(rels[i], rels[j]))
|
|
262
|
+
return false;
|
|
263
|
+
const all = unique(rels.flat());
|
|
264
|
+
const heads = [...cs.map((c) => c.anchorHead), ...all.map((r) => r.heldCounterpartHead)].filter((h) => h !== null);
|
|
265
|
+
return newestOf(heads, all.flatMap((r) => r.links)).length <= 1;
|
|
266
|
+
}
|
|
267
|
+
/** Where the newest active head among the entries' relationships was created (2180). */
|
|
268
|
+
function newestRelationship(state, cs) {
|
|
269
|
+
let best;
|
|
270
|
+
let at = -1;
|
|
271
|
+
for (const c of cs) {
|
|
272
|
+
for (const r of entryRelationships(state, c)) {
|
|
273
|
+
const i = state.entryOrder.heads.indexOf(r.id);
|
|
274
|
+
if (i > at) {
|
|
275
|
+
at = i;
|
|
276
|
+
best = r;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return best;
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* The atomic merge of entries (2140, 2150, 2160, 2180): one entry at the
|
|
284
|
+
* earliest position under the survivor's identity, the others its
|
|
285
|
+
* aliases; admitted where any was, so `k` admitted ones free `k−1` slots,
|
|
286
|
+
* which the next pending entries take in position order in the same step;
|
|
287
|
+
* every key of a merged id rewritten to the survivor; the anchor head
|
|
288
|
+
* settled (6560). Per-tuple keys stay with their tuples.
|
|
289
|
+
*/
|
|
290
|
+
export function mergeEntries(state, cs, bound) {
|
|
291
|
+
const withPos = cs.filter((c) => c.pos !== null).sort(byPos);
|
|
292
|
+
let survivor;
|
|
293
|
+
let id;
|
|
294
|
+
if (withPos.length) {
|
|
295
|
+
survivor = withPos[0];
|
|
296
|
+
id = survivor.id;
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
const r = newestRelationship(state, cs);
|
|
300
|
+
survivor = (r && cs.find((c) => c.relIds.includes(r.id))) ?? cs[0];
|
|
301
|
+
// the relationship's identity, where it names the survivor (2180)
|
|
302
|
+
id = r && findContact(state, r.contactId)?.id === survivor.id ? r.contactId : survivor.id;
|
|
303
|
+
}
|
|
304
|
+
return mergeInto(state, survivor, id, cs.filter((c) => c !== survivor), bound);
|
|
305
|
+
}
|
|
306
|
+
/** The merge onto a chosen survivor under identity `id` (its id or one of its aliases). */
|
|
307
|
+
export function mergeInto(state, survivor, id, gone, bound) {
|
|
308
|
+
const all = [survivor, ...gone];
|
|
309
|
+
const { settings, groupSettings } = mergeSettings(all);
|
|
310
|
+
const status = all.some((c) => c.status === 'admitted') ? 'admitted' : all.some((c) => c.status === 'pending') ? 'pending' : 'none';
|
|
311
|
+
const positioned = all.filter((c) => c.pos !== null).sort(byPos);
|
|
312
|
+
const merged = all.map((c) => c.id).filter((x) => x !== id);
|
|
313
|
+
const contacts = { ...state.contacts };
|
|
314
|
+
for (const c of all)
|
|
315
|
+
delete contacts[c.id];
|
|
316
|
+
contacts[id] = {
|
|
317
|
+
id, pos: positioned[0]?.pos ?? null, status,
|
|
318
|
+
aliases: unique(all.flatMap((c) => [c.id, ...c.aliases])).filter((x) => x !== id).sort(cmp),
|
|
319
|
+
relIds: unique(all.flatMap((c) => c.relIds)),
|
|
320
|
+
anchorHead: survivor.anchorHead ?? positioned.find((c) => c.anchorHead !== null)?.anchorHead ?? null,
|
|
321
|
+
settings, groupSettings,
|
|
322
|
+
};
|
|
323
|
+
const renames = [];
|
|
324
|
+
let next = rewriteIdentity({ ...state, contacts }, merged, id, { global: true, renames });
|
|
325
|
+
next = { ...next, contacts: { ...next.contacts, [id]: settleHead(next, next.contacts[id]) } };
|
|
326
|
+
// the class duties of the merged settings (2160, 2850, 6410): per
|
|
327
|
+
// relationship against the entry it sent under before (2146)
|
|
328
|
+
const f = followClasses(state, admitPending(next, bound));
|
|
329
|
+
next = f.state;
|
|
330
|
+
return {
|
|
331
|
+
state: next, events: [{ type: 'contact.merged', contactId: id, merged: [...merged].sort(cmp) }],
|
|
332
|
+
...(f.duties.length ? { duties: f.duties } : {}), renames, survivor: id,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* The merge duties a commit on `relId` opens (2230): where the
|
|
337
|
+
* relationship stands behind another entry (2145) that is now mergeable
|
|
338
|
+
* with its own, and, with `net`, the convergence net of rung 2 for an
|
|
339
|
+
* accepted anchor mapping (6520–6605). One duty for every entry that
|
|
340
|
+
* joins, in position order while they stay mergeable together (2150).
|
|
341
|
+
*/
|
|
342
|
+
export function mergeDutiesAfter(before, after, relId, opts) {
|
|
343
|
+
const none = { state: after, duties: [] };
|
|
344
|
+
const rel = after.relationships[relId];
|
|
345
|
+
const E = rel && entryOf(after, rel);
|
|
346
|
+
if (!E || E.pos === null || E.anchorHead === null)
|
|
347
|
+
return none;
|
|
348
|
+
const eRels = entryRelationships(after, E);
|
|
349
|
+
const others = Object.values(after.contacts).filter((x) => x.id !== E.id && x.pos !== null && x.anchorHead !== null).sort(byPos);
|
|
350
|
+
const picked = [];
|
|
351
|
+
const take = (x) => { if (!picked.includes(x) && mergeable(after, [E, ...picked, x]))
|
|
352
|
+
picked.push(x); };
|
|
353
|
+
// 2145: the other entry behind this relationship, once comparable and the binding rule admits
|
|
354
|
+
for (const t of rel.tuples) {
|
|
355
|
+
const x = findContact(after, t.entryId);
|
|
356
|
+
if (x && x.id !== E.id && x.pos !== null && x.anchorHead !== null)
|
|
357
|
+
take(x);
|
|
358
|
+
}
|
|
359
|
+
if (opts.net) {
|
|
360
|
+
// the incoming relationship, where it does not lag in its own entry
|
|
361
|
+
// (6550). Head rule (b) against the knowledge the entry held before
|
|
362
|
+
// this commit: its relationships' links with this relationship's taken
|
|
363
|
+
// from before the 6610 union of this verification. What the mapping
|
|
364
|
+
// newly carries counts for (a) only (the 6590 rationale reading)
|
|
365
|
+
if (rel.heldCounterpartHead === E.anchorHead) {
|
|
366
|
+
const relB = Object.hasOwn(before.relationships, relId) ? before.relationships[relId] : rel;
|
|
367
|
+
const own = supersededIn(eRels.map((r) => (r.id === relId ? relB : r)));
|
|
368
|
+
for (const x of others)
|
|
369
|
+
if (joins(rel, x, entryRelationships(after, x), own))
|
|
370
|
+
take(x);
|
|
371
|
+
}
|
|
372
|
+
// 6580: E's anchor head changed — every relationship of another entry against E
|
|
373
|
+
if (findContact(before, E.id)?.anchorHead !== E.anchorHead) {
|
|
374
|
+
for (const x of others) {
|
|
375
|
+
const xs = entryRelationships(after, x);
|
|
376
|
+
if (feeding(after, x).some((r) => r.heldCounterpartHead === x.anchorHead && joins(r, E, eRels, supersededIn(xs))))
|
|
377
|
+
take(x);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
if (!picked.length)
|
|
382
|
+
return none;
|
|
383
|
+
const cs = [E, ...picked];
|
|
384
|
+
const ids = cs.map((c) => c.id).sort(cmp);
|
|
385
|
+
const id = 'merge:' + ids.join('|');
|
|
386
|
+
const duty = { kind: 'merge', v: 1, id, entryIds: ids, relId, witness: witnessOf(after, cs) };
|
|
387
|
+
// a duty open for the same entries takes the new admission's witness
|
|
388
|
+
if (Object.hasOwn(after.duties, id))
|
|
389
|
+
return { state: { ...after, duties: { ...after.duties, [id]: { ...after.duties[id], ...duty } } }, duties: [] };
|
|
390
|
+
return { state: after, duties: [duty] };
|
|
391
|
+
}
|
|
392
|
+
/** The heads and superseded sets a merge is admitted against. */
|
|
393
|
+
function witnessOf(state, cs) {
|
|
394
|
+
const heads = {};
|
|
395
|
+
const superseded = {};
|
|
396
|
+
for (const c of cs)
|
|
397
|
+
heads[c.id] = c.anchorHead;
|
|
398
|
+
for (const y of cs) {
|
|
399
|
+
const sup = supersededIn(entryRelationships(state, y));
|
|
400
|
+
superseded[y.id] = unique(cs.filter((x) => x !== y && sup.has(x.anchorHead)).map((x) => x.anchorHead)).sort(cmp);
|
|
401
|
+
}
|
|
402
|
+
return { heads, superseded };
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Whether the merge a duty was opened for still stands (6540, 6550,
|
|
406
|
+
* 6605): every entry still holds the anchor head it was admitted with, no
|
|
407
|
+
* entry holds another's head as superseded beyond what it did then, the
|
|
408
|
+
* relationship that opened it is still one of theirs, and the entries are
|
|
409
|
+
* mergeable (the binding rule, one anchor head).
|
|
410
|
+
*/
|
|
411
|
+
function witnessHolds(state, d, cs) {
|
|
412
|
+
const w = d.witness;
|
|
413
|
+
if (cs.some((c) => !Object.hasOwn(w.heads, c.id) || c.anchorHead !== w.heads[c.id]))
|
|
414
|
+
return false;
|
|
415
|
+
if (!cs.some((c) => c.relIds.includes(d.relId)))
|
|
416
|
+
return false;
|
|
417
|
+
for (const y of cs) {
|
|
418
|
+
const sup = supersededIn(entryRelationships(state, y));
|
|
419
|
+
for (const x of cs)
|
|
420
|
+
if (x !== y && sup.has(x.anchorHead) && !(w.superseded[y.id] ?? []).includes(x.anchorHead))
|
|
421
|
+
return false;
|
|
422
|
+
}
|
|
423
|
+
return mergeable(state, cs);
|
|
424
|
+
}
|
|
425
|
+
/** merge/1: the merge as one step after the verification commit (2230),
|
|
426
|
+
* re-checked against its witness; where it no longer holds, the duty is
|
|
427
|
+
* dropped without a trace (a later verification may open another). */
|
|
428
|
+
export const mergeDuty = (bound) => ({
|
|
429
|
+
v: 1,
|
|
430
|
+
apply(state, duty) {
|
|
431
|
+
const d = duty;
|
|
432
|
+
const cs = unique(d.entryIds.map((x) => findContact(state, x)).filter((c) => c !== undefined));
|
|
433
|
+
// the state moved since the commit that opened it: nothing (left) to merge
|
|
434
|
+
if (cs.length < 2 || cs.some((c) => c.pos === null) || !witnessHolds(state, d, cs))
|
|
435
|
+
return { state: state };
|
|
436
|
+
const r = mergeEntries(state, cs, bound);
|
|
437
|
+
return { state: r.state, ...(r.events ? { events: r.events } : {}), ...(r.duties ? { duties: r.duties } : {}), ...(r.renames ? { renames: r.renames } : {}) };
|
|
438
|
+
},
|
|
439
|
+
});
|
|
440
|
+
// ── split (2080–2115) ──────────────────────────────────────────────────
|
|
441
|
+
/** The chain aliases of a relationship (2090): the identities of the
|
|
442
|
+
* husks chained into it. */
|
|
443
|
+
export function chainAliases(state, relId) {
|
|
444
|
+
const out = [];
|
|
445
|
+
for (const r of Object.values(state.relationships)) {
|
|
446
|
+
if (r.chainedInto === relId)
|
|
447
|
+
out.push(r.contactId, ...chainAliases(state, r.id));
|
|
448
|
+
}
|
|
449
|
+
return unique(out).sort(cmp);
|
|
450
|
+
}
|
|
451
|
+
/** The husks chained into `relId`, transitively. */
|
|
452
|
+
const husksOf = (state, relId) => Object.values(state.relationships).filter((r) => r.chainedInto === relId).flatMap((r) => [r.id, ...husksOf(state, r.id)]);
|
|
453
|
+
/**
|
|
454
|
+
* The identity an entry takes when the relationship whose identity it was
|
|
455
|
+
* leaves (2100): its oldest remaining relationship's in entry-commit order
|
|
456
|
+
* (firstVerified, unverified after, then active-head creation), among those
|
|
457
|
+
* whose identity resolves to this entry; the relationship's id where none
|
|
458
|
+
* does (a relationship behind two entries, 2145).
|
|
459
|
+
*/
|
|
460
|
+
function oldestIdentity(state, c, remaining, exclude) {
|
|
461
|
+
const heads = state.entryOrder.heads;
|
|
462
|
+
const rels = remaining.filter((id) => Object.hasOwn(state.relationships, id)).map((id) => state.relationships[id]).sort((a, b) => {
|
|
463
|
+
if (a.firstVerified !== null && b.firstVerified !== null) {
|
|
464
|
+
const d = cmpDec(a.firstVerified, b.firstVerified);
|
|
465
|
+
if (d)
|
|
466
|
+
return d;
|
|
467
|
+
}
|
|
468
|
+
if ((a.firstVerified === null) !== (b.firstVerified === null))
|
|
469
|
+
return a.firstVerified === null ? 1 : -1;
|
|
470
|
+
return (heads.indexOf(a.id) - heads.indexOf(b.id)) || cmp(a.id, b.id);
|
|
471
|
+
});
|
|
472
|
+
const own = rels.find((r) => !exclude.has(r.contactId) && (r.contactId === c.id || c.aliases.includes(r.contactId)));
|
|
473
|
+
return own ? own.contactId : rels[0].id;
|
|
474
|
+
}
|
|
475
|
+
/** The keys of the entry's relationships resolve through its new id (2100). */
|
|
476
|
+
const rename = (state, oldId, id, rels, renames) => rewriteIdentity(state, [oldId], id, { rels, global: true, baselines: true, renames });
|
|
477
|
+
/**
|
|
478
|
+
* 2080: the first verified mapping on `relId`, already in `mid`, leaves it
|
|
479
|
+
* holding no anchor the other relationships of its entry hold, or another
|
|
480
|
+
* binding for one they hold: the split, else null. The entry keeps its
|
|
481
|
+
* position, status, the remaining relationships with their aliases and
|
|
482
|
+
* its settings, frees no slot and settles its anchor head; the leaver
|
|
483
|
+
* takes its identity, its chain aliases and the settings of 2115, and
|
|
484
|
+
* holds no position until the caller's entry commit (2090).
|
|
485
|
+
*/
|
|
486
|
+
export function splitIfUnlinked(before, mid, relId) {
|
|
487
|
+
if (before.relationships[relId]?.heldCounterpartHead !== null)
|
|
488
|
+
return null;
|
|
489
|
+
const rel = mid.relationships[relId];
|
|
490
|
+
const E = entryOf(mid, rel);
|
|
491
|
+
if (!E)
|
|
492
|
+
return null;
|
|
493
|
+
const others = entryRelationships(mid, E).filter((r) => r.id !== relId);
|
|
494
|
+
const holding = others.filter((r) => r.heldCounterpartHead !== null);
|
|
495
|
+
// 2080 read literally would split every manual merge of unmapped
|
|
496
|
+
// relationships at the first mapping; open point for Trust 0.32. Here
|
|
497
|
+
// the relationship stays where the others hold no anchor to contradict
|
|
498
|
+
if (!holding.length)
|
|
499
|
+
return null;
|
|
500
|
+
const mine = anchorSet(rel);
|
|
501
|
+
const shares = holding.some((o) => [...anchorSet(o)].some((a) => mine.has(a)));
|
|
502
|
+
if (shares && !conflicts([rel], others))
|
|
503
|
+
return null;
|
|
504
|
+
return splitOff(mid, relId);
|
|
505
|
+
}
|
|
506
|
+
/** The split of `relId` from its entry (2080–2115). */
|
|
507
|
+
export function splitOff(state, relId) {
|
|
508
|
+
const rel = state.relationships[relId];
|
|
509
|
+
const E = entryOf(state, rel);
|
|
510
|
+
const identity = rel.contactId;
|
|
511
|
+
const chain = chainAliases(state, relId);
|
|
512
|
+
const leaverIds = new Set([identity, ...chain]);
|
|
513
|
+
const remaining = E.relIds.filter((id) => id !== relId);
|
|
514
|
+
const eId = leaverIds.has(E.id) ? oldestIdentity(state, E, remaining, leaverIds) : E.id;
|
|
515
|
+
// 2115: every class at default, except an `off` of the merged entry, which goes along
|
|
516
|
+
const settings = {};
|
|
517
|
+
for (const cls of CLASSES)
|
|
518
|
+
settings[cls] = E.settings[cls] === 'off' ? 'off' : 'default';
|
|
519
|
+
const groupSettings = {};
|
|
520
|
+
for (const [g, s] of Object.entries(E.groupSettings))
|
|
521
|
+
if (s === 'off')
|
|
522
|
+
groupSettings[g] = 'off';
|
|
523
|
+
const contacts = { ...state.contacts };
|
|
524
|
+
delete contacts[E.id];
|
|
525
|
+
contacts[eId] = { ...E, id: eId, relIds: remaining, aliases: E.aliases.filter((a) => !leaverIds.has(a) && a !== eId) };
|
|
526
|
+
contacts[identity] = {
|
|
527
|
+
id: identity, pos: null, status: 'none', aliases: chain.filter((a) => a !== identity), relIds: [relId], anchorHead: null, settings, groupSettings,
|
|
528
|
+
};
|
|
529
|
+
// the leaver's keys go to its own entry (2145 entryId, its duties and recipients)
|
|
530
|
+
const renames = [];
|
|
531
|
+
let next = rewriteIdentity({ ...state, contacts }, [E.id, ...E.aliases], identity, { rels: (id) => id === relId, renames });
|
|
532
|
+
// the remaining entry's keys that resolved through the old identity (2100)
|
|
533
|
+
if (eId !== E.id)
|
|
534
|
+
next = rename(next, E.id, eId, (id) => id !== relId, renames);
|
|
535
|
+
next = { ...next, contacts: { ...next.contacts, [eId]: settleHead(next, next.contacts[eId]) } };
|
|
536
|
+
// the leaver's classes start at default (2115): the trusted contacts' classes off with the anchor mapping (6410)
|
|
537
|
+
next = dropOff(next).state;
|
|
538
|
+
return { state: next, event: { type: 'contact.split', contactId: eId, leaver: identity, relationshipId: relId }, renames };
|
|
539
|
+
}
|
|
540
|
+
// ── departure (2210, 2250, 2260, 5860) ─────────────────────────────────
|
|
541
|
+
const needInput = (x, name) => {
|
|
542
|
+
if (x === null || typeof x !== 'object' || Array.isArray(x))
|
|
543
|
+
throw new TypeError(`${name} needs an input object`);
|
|
544
|
+
return x;
|
|
545
|
+
};
|
|
546
|
+
/** The matches of every accepted star without the entry ids `ids` (6160). */
|
|
547
|
+
function dropMatches(state, ids) {
|
|
548
|
+
if (!ids.length)
|
|
549
|
+
return state;
|
|
550
|
+
let relationships = null;
|
|
551
|
+
for (const [id, r] of Object.entries(state.relationships)) {
|
|
552
|
+
if (!r.tuples.some((t) => t.starIn.accepted?.matches.some((m) => ids.includes(m))))
|
|
553
|
+
continue;
|
|
554
|
+
const tuples = r.tuples.map((t) => {
|
|
555
|
+
const acc = t.starIn.accepted;
|
|
556
|
+
return acc && acc.matches.some((m) => ids.includes(m)) ? { ...t, starIn: { ...t.starIn, accepted: { ...acc, matches: acc.matches.filter((m) => !ids.includes(m)) } } } : t;
|
|
557
|
+
});
|
|
558
|
+
(relationships ??= { ...state.relationships })[id] = { ...r, tuples };
|
|
559
|
+
}
|
|
560
|
+
return relationships ? { ...state, relationships } : state;
|
|
561
|
+
}
|
|
562
|
+
/** The identities of a relationship: its own and its chain aliases. */
|
|
563
|
+
const identitiesOf = (state, relId) => [state.relationships[relId].contactId, ...chainAliases(state, relId)];
|
|
564
|
+
/**
|
|
565
|
+
* Departure: the relationships leave the state with their husks; an entry
|
|
566
|
+
* left without relationships is removed (pending: 2260; admitted: its
|
|
567
|
+
* slot frees, 2250), one that keeps some drops their aliases, takes a new
|
|
568
|
+
* identity where theirs was its own (2100) and settles its anchor head
|
|
569
|
+
* (2080); pending entries are admitted in position order in the same
|
|
570
|
+
* step, so the set changes once (5860).
|
|
571
|
+
*/
|
|
572
|
+
export function depart(state, input, bound) {
|
|
573
|
+
const c = findContact(state, input.contactId);
|
|
574
|
+
if (!c)
|
|
575
|
+
return { ok: false, code: 'unknown-contact' };
|
|
576
|
+
let relIds;
|
|
577
|
+
if (input.relationshipId !== undefined) {
|
|
578
|
+
if (!c.relIds.includes(input.relationshipId))
|
|
579
|
+
return { ok: false, code: 'unknown-contact' };
|
|
580
|
+
relIds = [input.relationshipId];
|
|
581
|
+
}
|
|
582
|
+
else if (input.contactId !== c.id) {
|
|
583
|
+
relIds = c.relIds.filter((rid) => Object.hasOwn(state.relationships, rid) && identitiesOf(state, rid).includes(input.contactId));
|
|
584
|
+
}
|
|
585
|
+
else
|
|
586
|
+
relIds = [...c.relIds];
|
|
587
|
+
const leaving = new Set(relIds.flatMap((rid) => [rid, ...husksOf(state, rid)]));
|
|
588
|
+
const goneIds = new Set([...relIds.flatMap((rid) => (Object.hasOwn(state.relationships, rid) ? identitiesOf(state, rid) : []))]);
|
|
589
|
+
if (input.contactId !== c.id && input.relationshipId === undefined)
|
|
590
|
+
goneIds.add(input.contactId); // an alias without a relationship
|
|
591
|
+
const relationships = {};
|
|
592
|
+
for (const [id, r] of Object.entries(state.relationships))
|
|
593
|
+
if (!leaving.has(id))
|
|
594
|
+
relationships[id] = r;
|
|
595
|
+
const contacts = {};
|
|
596
|
+
const renamed = [];
|
|
597
|
+
let removed = false;
|
|
598
|
+
const removedIds = [];
|
|
599
|
+
for (const x of Object.values(state.contacts)) {
|
|
600
|
+
const rest = x.relIds.filter((id) => !leaving.has(id));
|
|
601
|
+
if (rest.length === x.relIds.length && !x.aliases.some((a) => goneIds.has(a))) {
|
|
602
|
+
contacts[x.id] = x;
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
if (!rest.length) {
|
|
606
|
+
if (x.id === c.id)
|
|
607
|
+
removed = true;
|
|
608
|
+
removedIds.push(x.id);
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
const id = goneIds.has(x.id) ? oldestIdentity(state, x, rest, goneIds) : x.id;
|
|
612
|
+
if (id !== x.id)
|
|
613
|
+
renamed.push({ from: x.id, to: id, rels: rest });
|
|
614
|
+
contacts[id] = { ...x, id, relIds: rest, aliases: x.aliases.filter((a) => !goneIds.has(a) && a !== id) };
|
|
615
|
+
}
|
|
616
|
+
const duties = {};
|
|
617
|
+
for (const [id, d] of Object.entries(state.duties)) {
|
|
618
|
+
const relOf = d.relId;
|
|
619
|
+
// a duty of a kind (or version) this library does not know is kept as it is
|
|
620
|
+
if (typeof relOf === 'string' && leaving.has(relOf) && d.kind !== 'merge' && isKnownDuty(d))
|
|
621
|
+
continue;
|
|
622
|
+
duties[id] = d;
|
|
623
|
+
}
|
|
624
|
+
const rotations = {};
|
|
625
|
+
for (const [id, r] of Object.entries(state.rotations)) {
|
|
626
|
+
rotations[id] = Object.keys(r.awaitHead).some((k) => leaving.has(k))
|
|
627
|
+
? { ...r, awaitHead: Object.fromEntries(Object.entries(r.awaitHead).filter(([k]) => !leaving.has(k))) }
|
|
628
|
+
: r;
|
|
629
|
+
}
|
|
630
|
+
let next = {
|
|
631
|
+
...state, relationships, contacts, duties, rotations,
|
|
632
|
+
entryOrder: { ...state.entryOrder, heads: state.entryOrder.heads.filter((id) => !leaving.has(id)) },
|
|
633
|
+
};
|
|
634
|
+
// 6160: an accepted star matches the holder's contacts; one that left matches nothing
|
|
635
|
+
next = dropMatches(next, removedIds);
|
|
636
|
+
const renames = [];
|
|
637
|
+
for (const r of renamed)
|
|
638
|
+
next = rename(next, r.from, r.to, (id) => r.rels.includes(id), renames);
|
|
639
|
+
for (const x of Object.values(next.contacts)) {
|
|
640
|
+
const s = settleHead(next, x);
|
|
641
|
+
if (s !== x)
|
|
642
|
+
next = { ...next, contacts: { ...next.contacts, [x.id]: s } };
|
|
643
|
+
}
|
|
644
|
+
// 2850: a relationship that ceased to lag with the departure gets the classes on for it
|
|
645
|
+
const f = followClasses(state, admitPending(next, bound));
|
|
646
|
+
next = f.state;
|
|
647
|
+
return {
|
|
648
|
+
...(f.duties.length ? { duties: f.duties } : {}),
|
|
649
|
+
state: next, events: [{ type: 'contact.departed', contactId: c.id, relationshipIds: relIds.filter((id) => Object.hasOwn(state.relationships, id)).sort(cmp), removed }],
|
|
650
|
+
...(renames.length ? { renames } : {}),
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
// ── the commands ───────────────────────────────────────────────────────
|
|
654
|
+
export function entryCommands(bound) {
|
|
655
|
+
return {
|
|
656
|
+
// rung 3: the manual merge (6640)
|
|
657
|
+
[ENTRY_COMMANDS.merge]: {
|
|
658
|
+
human: true,
|
|
659
|
+
apply(state, _p, input) {
|
|
660
|
+
const i = needInput(input, ENTRY_COMMANDS.merge);
|
|
661
|
+
if (!Array.isArray(i.contactIds) || i.contactIds.some((x) => typeof x !== 'string' || x === ''))
|
|
662
|
+
throw new TypeError('trust.merge: contactIds must be non-empty strings');
|
|
663
|
+
const cs = [];
|
|
664
|
+
for (const x of i.contactIds) {
|
|
665
|
+
const c = findContact(state, x);
|
|
666
|
+
if (!c)
|
|
667
|
+
return { ok: false, code: 'unknown-contact' };
|
|
668
|
+
if (!cs.includes(c))
|
|
669
|
+
cs.push(c);
|
|
670
|
+
}
|
|
671
|
+
if (cs.length < 2)
|
|
672
|
+
return { state: state };
|
|
673
|
+
if (cs.filter((c) => c.pos !== null).length > 1)
|
|
674
|
+
return { ok: false, code: 'not-mergeable', ref: 'RLTP-TRU-6640' };
|
|
675
|
+
if (!mergeable(state, cs))
|
|
676
|
+
return { ok: false, code: 'not-mergeable', ref: 'RLTP-TRU-6605' };
|
|
677
|
+
const r = mergeEntries(state, cs, bound);
|
|
678
|
+
return { state: r.state, ...(r.events ? { events: r.events } : {}), ...(r.duties ? { duties: r.duties } : {}), ...(r.renames ? { renames: r.renames } : {}) };
|
|
679
|
+
},
|
|
680
|
+
},
|
|
681
|
+
[ENTRY_COMMANDS.depart]: {
|
|
682
|
+
human: true,
|
|
683
|
+
apply(state, _p, input) {
|
|
684
|
+
const i = needInput(input, ENTRY_COMMANDS.depart);
|
|
685
|
+
if (typeof i.contactId !== 'string' || i.contactId === '')
|
|
686
|
+
throw new TypeError('trust.depart: contactId must be a non-empty string');
|
|
687
|
+
if (i.relationshipId !== undefined && (typeof i.relationshipId !== 'string' || i.relationshipId === ''))
|
|
688
|
+
throw new TypeError('trust.depart: relationshipId must be a non-empty string');
|
|
689
|
+
return depart(state, i, bound);
|
|
690
|
+
},
|
|
691
|
+
},
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
//# sourceMappingURL=entries.js.map
|