cojson 0.13.18 → 0.13.20

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 (49) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +9 -0
  3. package/dist/PeerState.d.ts +1 -1
  4. package/dist/PeerState.d.ts.map +1 -1
  5. package/dist/PeerState.js +7 -36
  6. package/dist/PeerState.js.map +1 -1
  7. package/dist/coValueCore/coValueCore.d.ts +1 -1
  8. package/dist/coValueCore/coValueCore.d.ts.map +1 -1
  9. package/dist/coValueCore/coValueCore.js +11 -7
  10. package/dist/coValueCore/coValueCore.js.map +1 -1
  11. package/dist/localNode.d.ts.map +1 -1
  12. package/dist/localNode.js +10 -5
  13. package/dist/localNode.js.map +1 -1
  14. package/dist/streamUtils.d.ts +5 -5
  15. package/dist/streamUtils.d.ts.map +1 -1
  16. package/dist/streamUtils.js +5 -20
  17. package/dist/streamUtils.js.map +1 -1
  18. package/dist/sync.d.ts +6 -4
  19. package/dist/sync.d.ts.map +1 -1
  20. package/dist/sync.js +35 -19
  21. package/dist/sync.js.map +1 -1
  22. package/dist/tests/PeerState.test.js +0 -31
  23. package/dist/tests/PeerState.test.js.map +1 -1
  24. package/dist/tests/SyncStateManager.test.js +41 -6
  25. package/dist/tests/SyncStateManager.test.js.map +1 -1
  26. package/dist/tests/account.test.js +16 -0
  27. package/dist/tests/account.test.js.map +1 -1
  28. package/dist/tests/group.test.js.map +1 -1
  29. package/dist/tests/sync.auth.test.js +64 -15
  30. package/dist/tests/sync.auth.test.js.map +1 -1
  31. package/dist/tests/sync.load.test.js +2 -2
  32. package/dist/tests/sync.load.test.js.map +1 -1
  33. package/dist/tests/testUtils.d.ts +11 -2
  34. package/dist/tests/testUtils.d.ts.map +1 -1
  35. package/dist/tests/testUtils.js +27 -30
  36. package/dist/tests/testUtils.js.map +1 -1
  37. package/package.json +1 -1
  38. package/src/PeerState.ts +8 -40
  39. package/src/coValueCore/coValueCore.ts +14 -14
  40. package/src/localNode.ts +13 -6
  41. package/src/streamUtils.ts +7 -34
  42. package/src/sync.ts +51 -22
  43. package/src/tests/PeerState.test.ts +0 -37
  44. package/src/tests/SyncStateManager.test.ts +56 -6
  45. package/src/tests/account.test.ts +24 -0
  46. package/src/tests/group.test.ts +0 -1
  47. package/src/tests/sync.auth.test.ts +79 -21
  48. package/src/tests/sync.load.test.ts +3 -2
  49. package/src/tests/testUtils.ts +35 -34
package/package.json CHANGED
@@ -25,7 +25,7 @@
25
25
  },
26
26
  "type": "module",
27
27
  "license": "MIT",
28
- "version": "0.13.18",
28
+ "version": "0.13.20",
29
29
  "devDependencies": {
30
30
  "@opentelemetry/sdk-metrics": "^2.0.0",
31
31
  "typescript": "5.6.2"
package/src/PeerState.ts CHANGED
@@ -23,15 +23,7 @@ export class PeerState {
23
23
  });
24
24
 
25
25
  this._knownStates = knownStates?.clone() ?? new PeerKnownStates();
26
-
27
- // We assume that exchanges with storage peers are always successful
28
- // hence we don't need to differentiate between knownStates and optimisticKnownStates
29
- if (peer.role === "storage") {
30
- this._optimisticKnownStates = "assumeInfallible";
31
- } else {
32
- this._optimisticKnownStates =
33
- knownStates?.clone() ?? new PeerKnownStates();
34
- }
26
+ this._optimisticKnownStates = knownStates?.clone() ?? new PeerKnownStates();
35
27
  }
36
28
 
37
29
  /**
@@ -52,13 +44,9 @@ export class PeerState {
52
44
  * The main difference with knownState is that this is updated when the content is sent to the peer without
53
45
  * waiting for any acknowledgement from the peer.
54
46
  */
55
- readonly _optimisticKnownStates: PeerKnownStates | "assumeInfallible";
47
+ readonly _optimisticKnownStates: PeerKnownStates;
56
48
 
57
49
  get optimisticKnownStates(): ReadonlyPeerKnownStates {
58
- if (this._optimisticKnownStates === "assumeInfallible") {
59
- return this.knownStates;
60
- }
61
-
62
50
  return this._optimisticKnownStates;
63
51
  }
64
52
 
@@ -76,53 +64,33 @@ export class PeerState {
76
64
 
77
65
  updateHeader(id: RawCoID, header: boolean) {
78
66
  this._knownStates.updateHeader(id, header);
79
-
80
- if (this._optimisticKnownStates !== "assumeInfallible") {
81
- this._optimisticKnownStates.updateHeader(id, header);
82
- }
67
+ this._optimisticKnownStates.updateHeader(id, header);
83
68
  }
84
69
 
85
70
  combineWith(id: RawCoID, value: CoValueKnownState) {
86
71
  this._knownStates.combineWith(id, value);
87
-
88
- if (this._optimisticKnownStates !== "assumeInfallible") {
89
- this._optimisticKnownStates.combineWith(id, value);
90
- }
72
+ this._optimisticKnownStates.combineWith(id, value);
91
73
  }
92
74
 
93
75
  combineOptimisticWith(id: RawCoID, value: CoValueKnownState) {
94
- if (this._optimisticKnownStates === "assumeInfallible") {
95
- this._knownStates.combineWith(id, value);
96
- } else {
97
- this._optimisticKnownStates.combineWith(id, value);
98
- }
76
+ this._optimisticKnownStates.combineWith(id, value);
99
77
  }
100
78
 
101
79
  updateSessionCounter(id: RawCoID, sessionId: SessionID, value: number) {
102
80
  this._knownStates.updateSessionCounter(id, sessionId, value);
103
-
104
- if (this._optimisticKnownStates !== "assumeInfallible") {
105
- this._optimisticKnownStates.updateSessionCounter(id, sessionId, value);
106
- }
81
+ this._optimisticKnownStates.updateSessionCounter(id, sessionId, value);
107
82
  }
108
83
 
109
84
  setKnownState(id: RawCoID, knownState: CoValueKnownState | "empty") {
110
85
  this._knownStates.set(id, knownState);
111
-
112
- if (this._optimisticKnownStates !== "assumeInfallible") {
113
- this._optimisticKnownStates.set(id, knownState);
114
- }
86
+ this._optimisticKnownStates.set(id, knownState);
115
87
  }
116
88
 
117
89
  setOptimisticKnownState(
118
90
  id: RawCoID,
119
91
  knownState: CoValueKnownState | "empty",
120
92
  ) {
121
- if (this._optimisticKnownStates === "assumeInfallible") {
122
- this._knownStates.set(id, knownState);
123
- } else {
124
- this._optimisticKnownStates.set(id, knownState);
125
- }
93
+ this._optimisticKnownStates.set(id, knownState);
126
94
  }
127
95
 
128
96
  get id() {
@@ -2,11 +2,7 @@ import { UpDownCounter, ValueType, metrics } from "@opentelemetry/api";
2
2
  import { Result, err } from "neverthrow";
3
3
  import { PeerState } from "../PeerState.js";
4
4
  import { RawCoValue } from "../coValue.js";
5
- import {
6
- ControlledAccount,
7
- ControlledAccountOrAgent,
8
- RawAccountID,
9
- } from "../coValues/account.js";
5
+ import { ControlledAccountOrAgent, RawAccountID } from "../coValues/account.js";
10
6
  import { RawGroup } from "../coValues/group.js";
11
7
  import { coreToCoValue } from "../coreToCoValue.js";
12
8
  import {
@@ -656,15 +652,15 @@ export class CoValueCore {
656
652
  a: Pick<DecryptedTransaction, "madeAt" | "txID">,
657
653
  b: Pick<DecryptedTransaction, "madeAt" | "txID">,
658
654
  ) {
659
- return (
660
- a.madeAt - b.madeAt ||
661
- (a.txID.sessionID === b.txID.sessionID
662
- ? 0
663
- : a.txID.sessionID < b.txID.sessionID
664
- ? -1
665
- : 1) ||
666
- a.txID.txIndex - b.txID.txIndex
667
- );
655
+ if (a.madeAt !== b.madeAt) {
656
+ return a.madeAt - b.madeAt;
657
+ }
658
+
659
+ if (a.txID.sessionID === b.txID.sessionID) {
660
+ return a.txID.txIndex - b.txID.txIndex;
661
+ }
662
+
663
+ return 0;
668
664
  }
669
665
 
670
666
  getCurrentReadKey(): {
@@ -1006,6 +1002,10 @@ export class CoValueCore {
1006
1002
  const waitingForPeer = new Promise<void>((resolve) => {
1007
1003
  const markNotFound = () => {
1008
1004
  if (this.peers.get(peer.id)?.type === "pending") {
1005
+ logger.warn("Timeout waiting for peer to load coValue", {
1006
+ id: this.id,
1007
+ peerID: peer.id,
1008
+ });
1009
1009
  this.markNotFoundInPeer(peer.id);
1010
1010
  }
1011
1011
  };
package/src/localNode.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Result, ResultAsync, err, ok, okAsync } from "neverthrow";
1
+ import { Result, err, ok } from "neverthrow";
2
2
  import { CoID } from "./coValue.js";
3
3
  import { RawCoValue } from "./coValue.js";
4
4
  import {
@@ -223,10 +223,19 @@ export class LocalNode {
223
223
  account.set("profile", profile.id, "trusting");
224
224
  }
225
225
 
226
- if (!account.get("profile")) {
226
+ const profileId = account.get("profile");
227
+
228
+ if (!profileId) {
227
229
  throw new Error("Must set account profile in initial migration");
228
230
  }
229
231
 
232
+ if (node.syncManager.hasStoragePeers()) {
233
+ await Promise.all([
234
+ node.syncManager.waitForStorageSync(account.id),
235
+ node.syncManager.waitForStorageSync(profileId),
236
+ ]);
237
+ }
238
+
230
239
  return {
231
240
  node,
232
241
  accountID: account.id,
@@ -272,11 +281,9 @@ export class LocalNode {
272
281
  if (!profileID) {
273
282
  throw new Error("Account has no profile");
274
283
  }
275
- const profile = await node.load(profileID);
276
284
 
277
- if (profile === "unavailable") {
278
- throw new Error("Profile unavailable from all peers");
279
- }
285
+ // Preload the profile
286
+ await node.load(profileID);
280
287
 
281
288
  if (migration) {
282
289
  await migration(account, node);
@@ -6,23 +6,17 @@ export function connectedPeers(
6
6
  peer1id: PeerID,
7
7
  peer2id: PeerID,
8
8
  {
9
- trace = false,
10
9
  peer1role = "client",
11
10
  peer2role = "client",
12
11
  crashOnClose = false,
13
12
  }: {
14
- trace?: boolean;
15
13
  peer1role?: Peer["role"];
16
14
  peer2role?: Peer["role"];
17
15
  crashOnClose?: boolean;
18
16
  } = {},
19
17
  ): [Peer, Peer] {
20
- const [from1to2Rx, from1to2Tx] = newQueuePair(
21
- trace ? { traceAs: `${peer1id} -> ${peer2id}` } : undefined,
22
- );
23
- const [from2to1Rx, from2to1Tx] = newQueuePair(
24
- trace ? { traceAs: `${peer2id} -> ${peer1id}` } : undefined,
25
- );
18
+ const [from1to2Rx, from1to2Tx] = newQueuePair();
19
+ const [from2to1Rx, from2to1Tx] = newQueuePair();
26
20
 
27
21
  const peer2AsPeer: Peer = {
28
22
  id: peer2id,
@@ -43,32 +37,11 @@ export function connectedPeers(
43
37
  return [peer1AsPeer, peer2AsPeer];
44
38
  }
45
39
 
46
- export function newQueuePair(
47
- options: { traceAs?: string } = {},
48
- ): [AsyncIterable<SyncMessage>, Channel<SyncMessage>] {
40
+ export function newQueuePair(): [
41
+ AsyncIterable<SyncMessage>,
42
+ Channel<SyncMessage>,
43
+ ] {
49
44
  const channel = new Channel<SyncMessage>();
50
45
 
51
- if (options.traceAs) {
52
- return [
53
- (async function* () {
54
- for await (const msg of channel) {
55
- console.debug(
56
- options.traceAs,
57
- JSON.stringify(
58
- msg,
59
- (k, v) =>
60
- k === "changes" || k === "encryptedChanges"
61
- ? v.slice(0, 20) + "..."
62
- : v,
63
- 2,
64
- ),
65
- );
66
- yield msg;
67
- }
68
- })(),
69
- channel,
70
- ];
71
- } else {
72
- return [channel.wrap(), channel];
73
- }
46
+ return [channel.wrap(), channel];
74
47
  }
package/src/sync.ts CHANGED
@@ -159,6 +159,12 @@ export class SyncManager {
159
159
  );
160
160
  }
161
161
 
162
+ hasStoragePeers(): boolean {
163
+ return this.getPeers().some(
164
+ (peer) => peer.role === "storage" && !peer.closed,
165
+ );
166
+ }
167
+
162
168
  handleSyncMessage(msg: SyncMessage, peer: PeerState) {
163
169
  if (this.local.getCoValue(msg.id).isErroredInPeer(peer.id)) {
164
170
  logger.warn(
@@ -393,10 +399,12 @@ export class SyncManager {
393
399
 
394
400
  return;
395
401
  } else {
396
- // Should move the state to loading
397
- this.local.loadCoValueCore(msg.id, peer.id).catch((e) => {
398
- logger.error("Error loading coValue in handleLoad", { err: e });
399
- });
402
+ // Syncronously updates the state loading is possible
403
+ coValue
404
+ .loadFromPeers(this.getServerAndStoragePeers(peer.id))
405
+ .catch((e) => {
406
+ logger.error("Error loading coValue in handleLoad", { err: e });
407
+ });
400
408
  }
401
409
  }
402
410
 
@@ -622,28 +630,24 @@ export class SyncManager {
622
630
 
623
631
  handleUnsubscribe(_msg: DoneMessage) {}
624
632
 
625
- requestedSyncs = new Map<RawCoID, Promise<void>>();
626
-
627
- async requestCoValueSync(coValue: CoValueCore) {
628
- const promise = this.requestedSyncs.get(coValue.id);
633
+ requestedSyncs = new Set<RawCoID>();
634
+ requestCoValueSync(coValue: CoValueCore) {
635
+ if (this.requestedSyncs.has(coValue.id)) {
636
+ return;
637
+ }
629
638
 
630
- if (promise) {
631
- return promise;
632
- } else {
633
- const promise = new Promise<void>((resolve) => {
634
- queueMicrotask(() => {
635
- this.requestedSyncs.delete(coValue.id);
636
- this.syncCoValue(coValue);
637
- resolve();
638
- });
639
- });
639
+ queueMicrotask(() => {
640
+ if (this.requestedSyncs.has(coValue.id)) {
641
+ this.syncCoValue(coValue);
642
+ }
643
+ });
640
644
 
641
- this.requestedSyncs.set(coValue.id, promise);
642
- return promise;
643
- }
645
+ this.requestedSyncs.add(coValue.id);
644
646
  }
645
647
 
646
648
  async syncCoValue(coValue: CoValueCore) {
649
+ this.requestedSyncs.delete(coValue.id);
650
+
647
651
  for (const peer of this.peersInPriorityOrder()) {
648
652
  if (peer.closed) continue;
649
653
  if (coValue.isErroredInPeer(peer.id)) continue;
@@ -674,6 +678,21 @@ export class SyncManager {
674
678
  return true;
675
679
  }
676
680
 
681
+ const peerState = this.peers[peerId];
682
+
683
+ // The peer has been closed, so it isn't possible to sync
684
+ if (!peerState || peerState.closed) {
685
+ return true;
686
+ }
687
+
688
+ // The client isn't subscribed to the coValue, so we won't sync it
689
+ if (
690
+ peerState.role === "client" &&
691
+ !peerState.optimisticKnownStates.has(id)
692
+ ) {
693
+ return true;
694
+ }
695
+
677
696
  return new Promise((resolve, reject) => {
678
697
  const unsubscribe = this.syncState.subscribeToPeerUpdates(
679
698
  peerId,
@@ -693,10 +712,20 @@ export class SyncManager {
693
712
  });
694
713
  }
695
714
 
715
+ async waitForStorageSync(id: RawCoID, timeout = 30_000) {
716
+ const peers = this.getPeers();
717
+
718
+ await Promise.all(
719
+ peers
720
+ .filter((peer) => peer.role === "storage")
721
+ .map((peer) => this.waitForSyncWithPeer(peer.id, id, timeout)),
722
+ );
723
+ }
724
+
696
725
  async waitForSync(id: RawCoID, timeout = 30_000) {
697
726
  const peers = this.getPeers();
698
727
 
699
- return Promise.all(
728
+ await Promise.all(
700
729
  peers.map((peer) => this.waitForSyncWithPeer(peer.id, id, timeout)),
701
730
  );
702
731
  }
@@ -174,9 +174,6 @@ describe("PeerState", () => {
174
174
  test("should dispatch to both states", () => {
175
175
  const { peerState } = setup();
176
176
  const knownStatesSpy = vi.spyOn(peerState._knownStates, "set");
177
- if (peerState._optimisticKnownStates === "assumeInfallible") {
178
- throw new Error("Expected normal optimisticKnownStates");
179
- }
180
177
 
181
178
  const optimisticKnownStatesSpy = vi.spyOn(
182
179
  peerState._optimisticKnownStates,
@@ -195,40 +192,6 @@ describe("PeerState", () => {
195
192
  expect(optimisticKnownStatesSpy).toHaveBeenCalledWith("co_z1", state);
196
193
  });
197
194
 
198
- test("should use same reference for knownStates and optimisticKnownStates for storage peers", () => {
199
- const mockStoragePeer: Peer = {
200
- id: "test-storage-peer",
201
- role: "storage",
202
- priority: 1,
203
- crashOnClose: false,
204
- incoming: (async function* () {})(),
205
- outgoing: {
206
- push: vi.fn().mockResolvedValue(undefined),
207
- close: vi.fn(),
208
- },
209
- };
210
- const peerState = new PeerState(mockStoragePeer, undefined);
211
-
212
- // Verify they are the same reference
213
- expect(peerState.knownStates).toBe(peerState.optimisticKnownStates);
214
-
215
- // Verify that dispatching only updates one state
216
- const knownStatesSpy = vi.spyOn(peerState._knownStates, "set");
217
- expect(peerState._optimisticKnownStates).toBe("assumeInfallible");
218
-
219
- const state: CoValueKnownState = {
220
- id: "co_z1",
221
- header: false,
222
- sessions: {},
223
- };
224
-
225
- peerState.setKnownState("co_z1", state);
226
-
227
- // Only one dispatch should happen since they're the same reference
228
- expect(knownStatesSpy).toHaveBeenCalledTimes(1);
229
- expect(knownStatesSpy).toHaveBeenCalledWith("co_z1", state);
230
- });
231
-
232
195
  test("should use separate references for knownStates and optimisticKnownStates for non-storage peers", () => {
233
196
  const { peerState } = setup(); // Uses a regular peer
234
197
 
@@ -70,12 +70,12 @@ describe("SyncStateManager", () => {
70
70
  const map = group.createMap();
71
71
  map.set("key1", "value1", "trusting");
72
72
 
73
- const [clientStoragePeer] = connectedPeers("clientStorage", "unusedPeer", {
74
- peer1role: "client",
75
- peer2role: "server",
73
+ const [serverPeer] = connectedPeers("serverPeer", "unusedPeer", {
74
+ peer1role: "server",
75
+ peer2role: "client",
76
76
  });
77
77
 
78
- client.node.syncManager.addPeer(clientStoragePeer);
78
+ client.node.syncManager.addPeer(serverPeer);
79
79
 
80
80
  const subscriptionManager = client.node.syncManager.syncState;
81
81
 
@@ -86,7 +86,7 @@ describe("SyncStateManager", () => {
86
86
  updateToJazzCloudSpy,
87
87
  );
88
88
  const unsubscribe2 = subscriptionManager.subscribeToPeerUpdates(
89
- clientStoragePeer.id,
89
+ serverPeer.id,
90
90
  updateToStorageSpy,
91
91
  );
92
92
 
@@ -115,7 +115,7 @@ describe("SyncStateManager", () => {
115
115
  );
116
116
 
117
117
  expect(updateToStorageSpy).toHaveBeenLastCalledWith(
118
- emptyKnownState(map.core.id),
118
+ emptyKnownState(group.core.id),
119
119
  { uploaded: false },
120
120
  );
121
121
  });
@@ -247,4 +247,54 @@ describe("SyncStateManager", () => {
247
247
  ),
248
248
  ).toEqual({ uploaded: true });
249
249
  });
250
+
251
+ test("should skip closed peers", async () => {
252
+ const client = setupTestNode();
253
+ const { peerState } = client.connectToSyncServer();
254
+
255
+ peerState.gracefulShutdown();
256
+
257
+ const group = client.node.createGroup();
258
+ const map = group.createMap();
259
+
260
+ await expect(map.core.waitForSync()).resolves.toBeUndefined();
261
+ });
262
+
263
+ test("should skip client peers that are not subscribed to the coValue", async () => {
264
+ const server = setupTestNode({ isSyncServer: true });
265
+ const client = setupTestNode();
266
+
267
+ client.connectToSyncServer({
268
+ syncServer: server.node,
269
+ });
270
+
271
+ const group = server.node.createGroup();
272
+ const map = group.createMap();
273
+
274
+ await map.core.waitForSync();
275
+
276
+ expect(client.node.getCoValue(map.id).isAvailable()).toBe(false);
277
+ });
278
+
279
+ test("should wait for client peers that are subscribed to the coValue", async () => {
280
+ const server = setupTestNode({ isSyncServer: true });
281
+ const client = setupTestNode();
282
+
283
+ const { peerStateOnServer } = client.connectToSyncServer();
284
+
285
+ const group = server.node.createGroup();
286
+ const map = group.createMap();
287
+ map.set("key1", "value1", "trusting");
288
+
289
+ // Simulate the subscription to the coValue
290
+ peerStateOnServer.setKnownState(map.core.id, {
291
+ id: map.core.id,
292
+ header: true,
293
+ sessions: {},
294
+ });
295
+
296
+ await map.core.waitForSync();
297
+
298
+ expect(client.node.getCoValue(map.id).isAvailable()).toBe(true);
299
+ });
250
300
  });
@@ -1,7 +1,9 @@
1
1
  import { expect, test } from "vitest";
2
+ import { expectAccount } from "../coValues/account.js";
2
3
  import { WasmCrypto } from "../crypto/WasmCrypto.js";
3
4
  import { LocalNode } from "../localNode.js";
4
5
  import { connectedPeers } from "../streamUtils.js";
6
+ import { createMockStoragePeer } from "./testUtils.js";
5
7
 
6
8
  const Crypto = await WasmCrypto.create();
7
9
 
@@ -85,3 +87,25 @@ test("throws an error if the user tried to create an invite from an account", as
85
87
  "Cannot create invite from an account",
86
88
  );
87
89
  });
90
+
91
+ test("wait for storage sync before resolving withNewlyCreatedAccount", async () => {
92
+ const { storage, peer } = createMockStoragePeer({
93
+ peerId: "account-node",
94
+ });
95
+
96
+ const { node, accountID } = await LocalNode.withNewlyCreatedAccount({
97
+ creationProps: { name: "Hermes Puggington" },
98
+ crypto: Crypto,
99
+ peersToLoadFrom: [peer],
100
+ });
101
+
102
+ const account = storage.getCoValue(accountID);
103
+
104
+ expect(account.isAvailable()).toBe(true);
105
+
106
+ const profile = storage.getCoValue(
107
+ expectAccount(account.getCurrentContent()).get("profile")!,
108
+ );
109
+
110
+ expect(profile.isAvailable()).toBe(true);
111
+ });
@@ -750,7 +750,6 @@ describe("extend with role mapping", () => {
750
750
  const mapOnNode2 = await loadCoValueOrFail(node2.node, map.id);
751
751
 
752
752
  expect(mapOnNode2.get("test")).toEqual("Written from the admin");
753
-
754
753
  mapOnNode2.set("test", "Written from the inherited role");
755
754
  expect(mapOnNode2.get("test")).toEqual("Written from the inherited role");
756
755