From 343237e87c49fa366c3d31d2a10af4b517f4ab39 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 26 Sep 2022 21:53:36 -0600 Subject: [PATCH] Spec MSC3771: Threaded read receipts Note: this builds on a (as of writing) non-existent "threading" section, which is part of a different commit. --- content/client-server-api/modules/receipts.md | 125 +++++++++++++++++- data/event-schemas/schema/m.receipt.yaml | 7 + static/diagrams/threaded-dag-threads.drawio | 1 + static/diagrams/threaded-dag-threads.png | Bin 0 -> 18578 bytes static/diagrams/threaded-dag.drawio | 1 + static/diagrams/threaded-dag.png | Bin 0 -> 11621 bytes 6 files changed, 128 insertions(+), 6 deletions(-) create mode 100644 static/diagrams/threaded-dag-threads.drawio create mode 100644 static/diagrams/threaded-dag-threads.png create mode 100644 static/diagrams/threaded-dag.drawio create mode 100644 static/diagrams/threaded-dag.png diff --git a/content/client-server-api/modules/receipts.md b/content/client-server-api/modules/receipts.md index 9f80ff60..a13e2f9f 100644 --- a/content/client-server-api/modules/receipts.md +++ b/content/client-server-api/modules/receipts.md @@ -22,33 +22,68 @@ that the user had read all events *up to* the referenced event. See the [Receiving notifications](#receiving-notifications) section for more information on how read receipts affect notification counts. +{{< added-in v="1.4" >}} Read receipts exist in three major forms: +* Unthreaded: Denotes a read-up-to receipt regardless of threads. This is how + pre-threading read receipts worked. +* Threaded, main timeline: Denotes a read-up-to receipt for events not in a + particular thread. Identified by the thread ID `main`. +* Threaded, in a thread: Denotes a read-up-to receipt within a particular + thread. Identified by the event ID of the thread root. + +Threaded read receipts are discussed in further detail [below](#threaded-read-receipts). + #### Events -Each `user_id`, `receipt_type` pair must be associated with only a -single `event_id`. +{{< changed-in v="1.4" >}} Each `user_id`, `receipt_type`, and categorisation +(unthreaded, or `thread_id`) tuple must be associated with only a single +`event_id`. {{% event event="m.receipt" %}} #### Client behaviour +{{< changed-in v="1.4" >}} Altered to support threaded read receipts. + In `/sync`, receipts are listed under the `ephemeral` array of events for a given room. New receipts that come down the event streams are deltas which update existing mappings. Clients should replace older -receipt acknowledgements based on `user_id` and `receipt_type` pairs. +receipt acknowledgements based on `user_id`, `receipt_type`, and the +`thread_id` (if present). For example: Client receives m.receipt: user = @alice:example.com receipt_type = m.read event_id = $aaa:example.com + thread_id = undefined Client receives another m.receipt: user = @alice:example.com receipt_type = m.read event_id = $bbb:example.com + thread_id = main - The client should replace the older acknowledgement for $aaa:example.com with - this one for $bbb:example.com + The client does not replace any acknowledgements, yet. + + Client receives yet another m.receipt: + user = @alice:example.com + receipt_type = m.read + event_id = $ccc:example.com + thread_id = undefined + + The client replaces the older acknowledgement for $aaa:example.com + with this new one for $ccc:example.com, but does not replace the + acknowledgement for $bbb:example.com because it belongs to a thread. + + Client receives yet another m.receipt: + user = @alice:example.com + receipt_type = m.read + event_id = $ddd:example.com + thread_id = main + + Now the client replaces the older $bbb:example.com acknowledgement with + this new $ddd:example.com acknowledgement. The client does NOT replace the + older acknowledgement for $ccc:example.com as it is unthreaded. Clients should send read receipts when there is some certainty that the event in question has been **displayed** to the user. Simply receiving @@ -87,6 +122,84 @@ not have their notification counts rewound to that point in time. While uncommon, it is considered valid to have an `m.read` (public) receipt lag several messages behind the `m.read.private` receipt, for example. +##### Threaded read receipts + +{{% added-in v="1.4" %}} + +If a client does not use [threading](#threading), then they will simply only +send "unthreaded" read receipts which affect the whole room regardless of threads. + +Threading introduces a concept of multiple conversations being held in the same +room and thus deserve their own read receipts and notification counts. An event +is considered to be "in a thread" if it has either a `rel_type` of `m.thread` or +has child events with a `rel_type` of `m.thread` (in which case it'd be the +thread root). Events not in a thread but still in the room are considered to be +part of the "main timeline", or a special thread with an ID of `main`. + +A threaded read receipt is simply one which has a `thread_id` on it, targeting +either a thread root's event ID or `main` for the main timeline. + +The following is an example DAG for a room, with dotted lines showing event +relationships and solid lines showing topological ordering. + +![threaded-dag](/diagrams/threaded-dag.png) + +{{% boxes/note %}} +`m.reaction` relationships are not currently specified, but are shown here for +their conceptual place in a threaded DAG. They are currently proposed as +[MSC2677](https://github.com/matrix-org/matrix-spec-proposals/pull/2677). +{{% /boxes/note %}} + +This DAG can be represented as 3 threaded timelines, with `A` and `B` being thread +roots: + +![threaded-dag-threads](/diagrams/threaded-dag-threads.png) + +With this, we can demonstrate that: +* A threaded read receipt on `I` would mark `A`, `B`, and `I` as read. +* A threaded read receipt on `E` would mark `C` and `E` as read. +* An unthreaded read receipt on `D` would mark `A`, `B`, `C`, and `D` as read. + +Note that marking `A` as read with a threaded read receipt would not mean +that `C`, `E`, `G`, or `H` get marked as read: Thread A's timeline would need +its own threaded read receipt at `H` to accomplish that. + +The read receipts for the above 3 examples would be: + +```json +{ + "$I": { + "m.read": { + "@user:example.org": { + "ts": 1661384801651, + "thread_id": "main" // because `I` is not in a thread, but is a threaded receipt + } + } + }, + "$E": { + "m.read": { + "@user:example.org": { + "ts": 1661384801651, + "thread_id": "$A" // because `E` is in Thread `A` + } + } + }, + "$D": { + "m.read": { + "@user:example.org": { + "ts": 1661384801651 + // no `thread_id` because the receipt is *unthreaded* + } + } + } +} +``` + +Conditions on sending read receipts, such as using private read receipts, only +sending once an event is display, not being able to move backwards in time, etc +still apply to threaded and unthreaded read receipts. For example, a client might +send a private read receipt for a threaded event when the user expands that thread. + #### Server behaviour For efficiency, receipts SHOULD be batched into one event per room @@ -99,7 +212,7 @@ format of the EDUs are: { : { : { - : { } + : { } }, ... }, diff --git a/data/event-schemas/schema/m.receipt.yaml b/data/event-schemas/schema/m.receipt.yaml index b9ec7e4e..bde3d40c 100644 --- a/data/event-schemas/schema/m.receipt.yaml +++ b/data/event-schemas/schema/m.receipt.yaml @@ -38,6 +38,13 @@ properties: type: integer format: int64 description: The timestamp the receipt was sent at. + thread_id: + type: string + description: |- + The root thread event's ID (or `main`) for which + thread this receipt is intended to be under. If + not specified, the read receipt is *unthreaded* + (default). "m.read.private": type: object title: Own User diff --git a/static/diagrams/threaded-dag-threads.drawio b/static/diagrams/threaded-dag-threads.drawio new file mode 100644 index 00000000..2f3121d7 --- /dev/null +++ b/static/diagrams/threaded-dag-threads.drawio @@ -0,0 +1 @@ +7ZpdU+IwFIZ/DZc6bdNWuJQCujPquOvOrl452TbQaNowIXztr9+EJv0goLCirYwyo81JGpL3PQ8nU2mBIFlcMDiOr2mESMuxokUL9FqO41i+I/7IyDKL2JbvZZERw5GKFYE7/BfpgSo6xRGaVAZySgnH42owpGmKQl6JQcbovDpsSEn1XcdwhIzAXQiJGf2NIx5n0bZzVsQvER7F+p1tv5P1JFAPVjuZxDCi81II9FsgYJTy7CpZBIhI9bQu2X2DLb35whhK+S43/PIf6YMfPSfXNzfx1WP3ez/2ToDaxwySqdrxz5ghGInYudQaJ4jgFKkt8KXWhdFpGiE5td0C3XmMObobw1D2zkUqiFjME6K6hzTlA5hgIrPgEpEZ4jiEsgMTElBC2WpS0O/Jl4jPEJMjyDnBo1T0cTpW09ypJajty4FosVURO9dZZCiiCeJsKYbo9NTWqOT0dHteOO2c+acqY+Oyz201FKr8GuWzFxaIC+XCPo742x3pHrsjAFQdcS3TEdu3Nvhhv5sfruHHNcTpsTuRq7zUHJhO5G59jBO2YcS5If0rYsPJOCsSQ7yQBq2LHAR9byDW2D2Egp2qgsDeoOAGAcG76dc25EKRKHaqSRmP6YimkPSLaLeay8WYKyrTbhV8QpwvVeWGU06rkqMF5vfi2lLXD/JafKRmrd6i1NVb6kYq9nuvJ5CN0l2yWdy2aun7Kiz9FHhOxHZv0Fz8/kETmObGyn2/bKuQiU5ZiF7QUx1tOGQjxF/LWzNNGCKQ41l1HQc33TGg6TYZGsdvGDQueDM0T9NkrMenNEU1cFRC56GM1RaOCPyDSBeGz6PVTkpeDwL5eqlsHRAw8BkAAwZgwZsBOwRIbn5u1afbjlczSs4XSnWh5O6IklMnSuZJu9cElMBaTXLdumuSewwg7Xm2awhI3o4ggTpB8gyQ+o0AyW5eTbINYb5Q+iCU/B1RcutEyXwaN2gCSu76g7K6axLoGEIlpwzBkGOaHgNje577IjiJ8+XLxi3kHLF0FXEstzkYnr2toqmEPLFObfFTSUpbfbjvTKqa/ZZisYliCB0OJ2Jp6ymaL+L/s9b878dFE+g2C6Xv1lwoQXsD3yjC/BjY3rN+fh62Ozuy7dVZYs3KcdkECF3QvNOqZQhz/LQ1BCX9ILrZj1D0KkswfWsETO6HPdcXzeJLFdlBofhuCuj/Aw== \ No newline at end of file diff --git a/static/diagrams/threaded-dag-threads.png b/static/diagrams/threaded-dag-threads.png new file mode 100644 index 0000000000000000000000000000000000000000..4cf865edd5d3eeccf8a5cb3850a739f3847e84e8 GIT binary patch literal 18578 zcmd_S2{@GR`!|dzDj`WIYm$8#46>UU`@W7f`)=&R$kIagC0oi;L&}mVWXsSZCBhK0 zN68?BLB{r8Q{UhJ`ThRKd%W-Q9?$c>$8#KSeP!-@uKU{V>pIWR`8hw=H_60EoAEg3 zaVjb*MqM4485I>Z2KAaALZul;qC7Bw~UOGjEuOH zqPPsyLJA@X(U6g+`~#Jcm6NmmTi)5zJ>Z`Tb8P~qNnHAGB~aTyH%mboO?bQHW8q@zY=lDV+F(M16~O~=y>KZ{l!2CaFcKY! zQjpfu4m5z6m?{KV%J^&QBK%wpVcy2UGX8LL!>|aHucd(^BFI=)8qABPE@%g;8wt}l zkkNAqx3z_vq2;vo5lA=~E<)2I(9;|xA7Q5LY^7lvY3>=IBP)YK2WtE4oBGM?nYy~5 z!eN0S=I{V%8&@~9mo7%b#?)I++e5}kH$pBz%EcCA6=E6=H-jQTQ-RW<;c_O@dI${@ zE1xji2$+_)g0!x#A|k{Ssb>)h)e5xL)b-L;bTJK+v5|opxIpC4US9g(Npo2U=#7c4 zhdk2I!^GUf#t7pPA#H5}amRrBG8QP9a|AllJjg9L0wd??rL7x)40AJe)|2(|MCkj5 zD(Kr-d$}P3wY}kTp>9%I5F39(V;7VQS{f#U@wE){M)(-Q<@M#gFcy9qa$cUg!6u&Y zK)9`}k2gHT)6fF$r>P}l9cV6P3|iCkll3uy>lyk)!U8l=(iT##7J=4MijjeO&OV`$ zvIta&m8F@nqMNO)mbbT-yIzPs(gmexWMLg-Y$4^Q6Y6ekEaM8sVk z^n(?&!>log2xncewl!_EOi)l+nFvUrPH>2i2Ex?M)f{30cMF3Ug?o90%Y}FZhw11T zfu=(&!pwr87)u>JFJn1vIYl2$V;Q8Yxty+tR4_8cM9)|&#M~4bX{s3{W1yp9rsxt0 zvzCF$z&!(Hf;>ZD?kHmoZ4Fm#sJxr2j2v9wOkc-2$lT1|L>}qw?`vXi;B4UT9I0T9 z)Q=3daPhKIFbzb9Te(|nTT00X`vw^z zgA@b8!u8~|jHLWzBCXvbWt_tTp!(hzMTCx*afFU-xD*1T03P&(TlgEhXh#Ns-HC9~ z4>R?)l(Y3lKus+pq(hChO?@K3`au|2V~mWnwe(G8rTwIJOkE)m3vEw%3#6@fh+DWe z%rwj$W+-K$XyhAg=pJAMx0H58m}$w$Ya2?NIU8%4M8eE9-L*0DCRP@{f$lK0ft-7g zUx<#G23kSWS3?7B>xs4v(KJSz>AD2Sg-JVmxcKFc z5zrW>Y;lC9w3%yIC_K^z?QA6DsbwApmG^||%R5IHptRw-et~F=rmwcKvyP{c zkGmWcs_Wye2PR6>#vsBQ;;UnTkV4qNLqajW$bev(P*jjV+{6H3?Bap2Ft-eJ0X+}5 z&<}D3?bzCShammDwKS!S^XlO-~sud7E%)KQzkJ zCIkkF^Z|%@1OEIlm+5@6fW+Po(fu(~{06 zoP_Z+q0Wh)DBwJ)!J9{WYW<8m^F{h(oj_jN%*F^tNjZxfS9O1h+@W!Q!>CH7!9vS- z`W>xs|LK5}#TD&B(H?b^^Q4f?#i{+U-1uiB8+Ng=_^rhuysxE|mDTZMw7gW*^f}_! zZ4=x4-sh!jP|+|4((__ysA-tv)C*YEN$#qzLYcq?6;|lzpR$V7FsC0!47Mp3ycieo z!2dP{<17h0SHcbC112tyH~42yzC?Ft321*OEex*j%$Go9z$j?V^dE#TkD0Z{M0-!Q zHrJ!39c%Dgw#_pMTxGi1mn!_~*a)bxdfl_)wioRh{DQuQu1p}uUib=qx+j-?l#w%0 z^E;@7H`9tni9;o9G3UGr#$xN|sK9t53VrG6WJ_4~m6awf%;sWlWx0?DIeK@it0ReZ zt_VGq^-$}!-r>Q17N+ve^-gsv8fS(ldn46I0sVZ5+-`}N1|B_k`TWP~8SLzd6 zUy0-erO;xpS;nQ@7#jQ^PI;LwmwTM(h@I}|k~CTd-ilfcMj zyDXNg1>U00omXAiLhfH*T^}gbW~!=%ieMaS3a+4Qs-4=oyM#C#KGRXtmnFn~Y(uY3 z$$0df^X*pXgyEo$VM3oyNVdKTHuTX()0*2fD{yvRM$gVNKutn{&XV$Cq3s(x~`APkgN5Slc z9mFn=z35FBRSCrfJvJ(;^nYZRVBKOy=jAv#QoV@~erz%7*;kwk-klh9xYt?pslN^CFVuv3^aXE9{`Q3B*PBBvk zt)``j^w!m{u=d7$BdY*9!TE@fedA{Fw@I!7RsfP_ANp1RmY1HsBG zVJhabkAAA%o@O81CD>a#xVzdeNmfqW?bi=uFEd*X-Tf*d+`Rlb<*MPfNG#jn@kS}HhUzp1u^ILZ)nGZv=R;Jshz3r>4p6W?FBGxAM z-mxsiSO}R|-zkT$e%xDo&!fiVv%NA|G_T>&SA3zVXr(b})>NbwSyaqYCaBBY-_`Tw zx$E5Kh?6l^>2dlOIL^3zaVYumRaUOIpDipdV;qhPPh9_YFc~fW;)-vLHLiI6j*c02 zf`>QublV9otmWYi2@#qYY$C(klIN2v2W7_em8hg1lJ6Gke`wbOajdegp?A*A4& zXOa&Vt8n1~Ws47?w#J1DrY|m-HmbMi3$3)jgsSL^PhJ!zzaihOA>{i!ZX2%%^e>Sk zpP{RMR8C|Z?M&n9ZVs70?W^$2i{du%qcxpL&#DT|~jwkIoh z2p1%s4#^|%Z{-ROhLE}0O@DOf({2e2p$ROlqQm3p$=}G*5&vvw^rQt^!)3(qpqCEE z?dxZ4WrYz;Sq_2maOoYt0Sx5V3@YsQ`y>mB$(7NkB6x-qy%Z?UCYF{NuVz$6DU*q$ zNmQ-V(tgVatkrLUIO2W~KN~1>z2~$#so;VV=P8O2Kiv6}InZKOvA} z2oHewi7nItMoFEwj+Qyv<92!i#g6^brXuWqgNabe6bQxbPCmkqc9j7=l4+dUxQHxBpiRf$z4_>ro}6LIvk4pFYgY zY*HdE2fvBLS}IZNXwi-F$Z8q4m0zPu@)d&K>?-zeG9(oT;k@1&EqI)z=+uVtLd@sN z>gv>?^q?baTpo#SvcgTj(|8Bmud5H-fRTn^bXen&g&V{{Iqv#PoYXL$bRGyfaH2B! zd8j$()v;#S`S4mh{y5^>>)NE5CO!;PK%%P4)hzr)P*g&SIa(ngk(;2MdcO8HA1GwP zs_~~3j7K{719t)ymPw0|aJ$qhQ*>GX%tUIMa>toWe(<^`u1b|My-(v%fshlkw6>(c z#Gmu&A82vElto;CHuj56}#wm2XWl;m$Sb$ z?}kfcy*7(uqj9E7B8Kd_RqOSrq+d|EDAM#mTlitcS9S^e%OO8{Sc(|-cKTvZ+4}h{ zlqNe$Cg^(@=SG#Q@iTIgV@piNnmdl(y>S|>q_;D@%bfeMMJfXN9)?*{YTRN@`0u{8 zU+1&kI&Ljoce7G1MboLFSL}BN^4l)P9_~)hwe7BCCnqNdma=NqJi(5nINwdV`j9yI zLGWn(8RzR@mXE14Vn+%V$wGPh@0xatUPL-nwU_H}R|=kJSFrTbPCp;y@vG*mr61-q z&dKQ%kLu6H%K{5;659gP4%!{IwIY9I4Ly{I^ZwyND<97(1p3I=`;-_~t`zw>ZsQ5# zt`ifjW1A^qduhaY80RDjOg#hictXfdbd-CA90t#Ou-a8R7i)N*oyd(i+jOqeXf1*M z7Si7%P4(mp&ZAN*F9%~>1$%m4{3z>l$K&RUAVSx!rw(`C<1 zkqG)!EMpb0S90V?^mxU>eIw4MbJqCo<3>o8V3jZ={Ik^wzbN&0)T2>CJuv#qlKXqf09l0+>Ix8Vgt(&&3cv|PuWS*<0^ z6{fW#IlK(Ve9^i97t8_kJA)8Puy=+zH6d_@HU}jA>mD2TgqH1H)b&};(({r7`$L4u zflu`2^>$;{c(Hw~`3k#w$SxLoQSuY<)F@r0g&1CPB1`6$kow?da^i=sVpkg769e!x zV#*DP0Nxl^&WE{|=-As?U@cZBP&5(n5imd$qw$D9U|6AOPTUIJZYRK+sAXCy=g z>hID?%|6SXJPmf)Ddj&m1^#WPD8@#)E4a;r&h+HUkrfaHG6R}agxg#U9$@!LmwF=p z$M#DD?fB4vcJdLFg?y}-rkc|JGsyqh{(EccidgMR5?pS}g=OiNv~vE=m0+q@Ww z?Kua6esVjlinQMauuf7KHW32A2o;j(s{HZ--XvtSA=iqdR@by%xl~YIWBdQxSv|5|n)XhbnUm zRidmGho6e>Zm)V;N%9BkO3_z{3NC~0_gMrGQdR6MO&8DIvdGm3 z7UiL{WZ{bzOPMOQSb3)WTQ{5d_MRK! zb`59t3)%tbG%+es6(`>u0_!@fPwmkD)Om%>?iZM*i_F(7sHs^rNDQ}LzllFF_xc!5 zj<9*T4jdkT#9{l|;oiJj{?5jHH-P+Zzkkgz)L(S4d?o1cO~dzNJ{7i6s-cD`Zc(%- zGIM8f=3x*Ac*Ei+xiv)@FG!rdktGtMK}E1T=W#26D1PgL<&V3RCw>ECYDa&ivPYU4 zhI0PJqJnK?4!0<91wQA^R{;zR#rN}x%zNPb;$Lf%5~no3n7XGk@M2Ef%pkJh@dVjJiM6Q>G z{`_J}>MH|?=U>38OzZY3-A%q=6^c_i#eY05&Y0$9`0}0SuJ1oC;o4Uq+9bElUB8yi4xz>X0D2cjtBe_;ep08!Ib%49o#C z$#Se9nuZ+pSp`GN1X0h$;R4X~&o9#6lPxdKD}`Q$K;6KU8~;o##oDv>b3Lbc6X^6c z^Lw)1ch02%U?#pg`LZ$O-N%pXjG)4#q9BVLUz!%bQc>=u5u9<(O_o1VADDtafk-{X zSyYRZsOtAKlZi%y{+}>H5RchBen&jmRsf6k+{BoNcO%Z zdJVsnd2Z-U-6d`>j|%}CmCf!L!MABA4*I99#Vn<(-be`;%_%`6i1wtBmR+@JNshjQ z7ZgI~d&6q3ddI{a2k)h$Pg$ggkvdvRZ`CfH19FK_Bp)A_mh32c4+MK58_shdb1QN| zm89n9r!E-g#OcL=$gTHBj`v`>pe-7UMZ32LOez(q#m0!_2Z6WO7=F|&X!Lad+&vJ1 z=$5EPSL;m?IiuHz3o^aWoZq@Fey*r^sgXiti5?wGNJsKLfI1FQg3N{n`|RJTQ%_A$ z{Q$|BPDRdN;7|&ELRy_xV`H-911tO8Q43ouT9We85*;{nw%9YT2a=w=sIc0V155PLe&kZ7`2GNLYnm;z` z0*0ojNEU?D`Dt8Gy~cpa@99@obgq@OTHk(}IOU(yCZRFoH{M_vzB(liTx8bZW8;+n zip$AST$@WH`C$F%?`|#6jrrojE&o3z!ZWlKd}`u751EPecTTr>@MTmZ8>&nF%iXZrV| z^zpfPtyiklpe`2t{zm3m^;ehwUJ%Sv-|$^6p#pU!P8~$*oZ!8h^Y?-%jF@451`IF7 zsvIhp(J@Qu{kg%(h;q!EbbS?-y8@MUMcLFi?$zc=tcJPVDN< zGNvhsJ!mde3e!vHRuPHX`kA#n-q;N`&?6AZ2fx37*1H-LHKE>n@Df`ZoYGr%W}61T zf|FeG2IWRYA_9n1op-u=0=3$dt!8vm+_BXr;UtF({s?7m$+66L{~Hq$6Fd;-$NJ@k zZ+>`vo=A4w?#keT8Z`t=-un=_P0;4i1A4-a1$1@hk#_>^TWUX5gB{p{Q`u6(f zu{l6@p||cV9Wrq%D-0j}{PO&5WVyz6HPq+x<*ju?t<|v?RXz9QMAxFXD1EacDcv8r z?{?(qNvY_V*oS*Bd`FbV6)bH;kSYd=geupX9^cc^GoBiQQA0eW4Nbz!i5q#&kbMdz za{mBOB4CLDh_?CS=u^AKJm3hBk>@He5tasX$+9b4sMZTywVhzapxIlHw?m~7#L$+^V=a!%h(Jg~G;$*m20odF5En!Pab1x4M zet*lj5-WL-fMvJXmmh5Kx7z`C;Dg>#n|7&D4~JCdSD+l$#CQI<07UGdAyn}isR>MA zk$CMpW&0liyT`{%a`rYKRswGC!+lhnu;D#f-AY>xVCVC(BSrE`b+%1`&MrcB&TzRT z$5oCew~8Zwa72F3(mwwX>Oc0GrJ!bQaayu{Z+)YU`b@IX#^R@TO0aCCxQgE{uIqT{ z9g5WHHf>&7@4kNISmBBIC=Fph*NV}XQCa=9m3E6klAQ6=CiVp1o%qc~I*sus`=1Yd zrb&VM*Yq~4J`beb@0~NZNpMs?F3AiKPN;;^BZ&ZA+M5!6!WkEN$~6CUlSkGoGlX@x zvo?(er%S4VotP_Q3T%d0p)XasH?h;OwI4XlIX1;ODpXk)OkdmhH2>6h7nv;F_Sj^S zGdc+Q2**a z`uJlHf=A|9tT6zUD{O@2dmV2_Hif+xBtI^RY4Pc*XmVOHvkb$c^K(@9NrRQDFQ;85 z+yxtZ?gfb4mwgs9&Uq!gy7hp(bg4=a=jTBgHv|(+wsd!268@q9a{^J;vD%0WcoR$w zaT3KC<3W0-e|~6)E1#I+Ns6al^h|SXUpupf%)guLNlM~qfWgUBxiR-Yt7R)u_Ua?z z(&q|+_Zd8m!2-j+3!RgW3vz(p;uLo59u~ZSuFzbCH`VPv@g=RmI~@3hCtNu4iKW$q z{Ck8d;$S@j2X*6u28aQRsQ+<8Z1P#Cd31H|#z5MZVLr9rg`ghdi{+tZBjE{TX}2O} zbv%mP_|bc>{>u2!3$)tHfaDS43J+Jv^=jpljhZjXHXz3V z`pAP6&u+aRyM5`|OW(^23jLl`BBG+KRe6-K+rF3RpQ8F4QkZO}OM!)X!Ntvp*T`Ob z=IERcyeJC*sYPBh?{p)*_yR55g*_z_RrTC5p&}q4$-SDmHz!ultiT+7#_M~091}pN z$I6Et>BILOk|C$riM_`~MwmR;_q8$T~3`G~2;o zXBjnoA1aYJ1-UqD3|wsVo~dO^N11V8Bb)aj{Ap{DZcFDfAR?t}0A(Z4mhXDa}@Zo=Pmf2u9z4fXEL=dzSzy#cdfh9Uknu zf6NiqRg@xN*4J|{P(1x%#<{BqC$ifUyT+PpsZaq`6$sFzqkP@J9hu~D5IZwUpiXylw0K59D;>|@{&)S+pWOF>%mAi;L9y9YM zUdFTc{QSe$YKJ?0!<|P7_u+kDLh+urRCa&X4K$*h3RI&pqdh%nd{Y&JU%Hjx4@-`FC7TdA%)A20PYn=O_ z*hPnyP=?PPVjUU}PFZH31IR%=s0``__4~q0fy<54F?I**`RyM-&dD7h`s*LNLR{Kc zS*imTJT@1HYC&VKAQ-$l&Z*t8w@@)#Z!f0#mP4D!kTR-PskOUPF76^)@qmtNSrz4^PWrU zB#$l%ER}y_2J{gyR4`BUObu6b2?2zG)oEw3&=d*;u4Bm?7^veVD-d$WRmn|Xz6+iL zhB;%Yu**U*Qt|bq#`7Vkb`6$L#A>5GF0`TP;-nLpJ$!F`?VhrvkOCH`cx~hKF|bs@V2bBW*#DdRf?OqRomDD&JVuasr(Mc^F@PjQ@15tfA%uBwdzFU3CdyNlM za>U%IE8=QpA!5+UiNZ$fKGGz4=u$S}pPnm$-SUbTQ}^vUsB)BE-+LiZ)#1oZ;-_>< zFzO*7_UMrSIh64v7J7vn^x}Tnvz)wiz{5%5Esy_2SY_t1v^ss2dcy= zn$T`beoXg~%#B4rGf)BJ{J^f}G{KHLuFx8=3ABK^pqdQMO`KY%`rZn9&PT9mPjPBz{X$rDLvtF=+%?*RbX$_ zhRhog)D(F$FJ8|UGS+*aegS@Hu&7Srzd_7C-bnf!@=xB0LXRwXpAqk(`;(IWSJtSy z9jqUrvwp9~hk?rNuNJ_&(u=~9R8v?IZ=bXNUDVettE?2KFHv$?HYk9#jm5qUSP&@r zVZh4O2F|>Zl}l;(9{1|k%-cuW8Qd~%A2K^Ilt|v?bzXTgNT*)<2bTwF$r~Oq|KU&g zPlDvXDz!2NVNFp1TQ}63t6<$fLt>l=?E=BH(3mdRfUJy7vX%`XG~<8<6#7lJ$QgdZ zin|*;0A^mscUWv;sLC3PhXDsih|{-oSE?GhUrUMC>;}Dz3s`KPUA-Q{D!(w=>H>;KUp=+=`s-?Y1MH{N`4pPUIpMi42`&5eGbd$kAl(TYX%tDX# zPBe!U6`W-{xDf?lLW~y{vi#~U85jSYxM{z|`Fd0;bfALk{?w!ak$l-SF?Q9@Bb;rNDJyU71hk)>CVeoN=Wmg^IFyreC z8$E||%1Jq7eh|o$Ujod;+|j3eospNVP^~_c*z?O+Frz*co|Qn{2wdD*EuDH$q!5H{ z-RQj{6G}8$m~K~NvfO!}Kb}LD`V80(w}pX6!K3c^U8x+9>WK((IVuW%%pPYQtoqi{ z7em0p!8<+sM~VOHAa$*K^C)9xJNqk{1N%oYi=TxRF{CGT*&cw|s15ptSQvhK zenzkVSc9TMM^j-Zj;07+}DWz_9obcwOx_*+vnyB7%*3SkPyp&S^*2rT668X#z zidVz6t|$MTd}$359AW^O=Vz!L*r)=hdotmiVS@S=19A7+;!u@PzI||#tpAvWZG+!J z>kKJsdbfDPC$*j@QI!s>29goVJKwX{NrT2$Tt;3slNYPzu1xQwZzOI)+twK8gO*N6 zH1RI4!FG8~@@N54o9iinPz*Go>uzx!s&y@4V=(UasSCl5vHif;&*ye{D>LKa1hvDx z4J-~agnr_cIT7(K4KQ5!vYib6AcI-S_X{w~iEUKILO!c)yEPUT79WU&(z)3bNh3xG z45dN368XF#^}EDibo(K?S$!5+UsEFB)B*Z<=>k@fU` z6|QY@FYz6lwLRbVbZon<&*u+6n#`f#399SUdq1Eu-UAosM?RJt6x>lydsGD|{ltTc z$Hl=hyb(Wo3d9C&PK1dh#9g3iNr)YBKr3>I6i3W5RDjh!%cwz0faBUt${-Q@;${K$ zm$;`b=&yff?w=cLoWkf+GD@KL2K5e!Qx8CfMk&P!k9koKXh*{JM>1d~MALF(hII^}Q@~&W#);I~M z4-}w7lxX{oWy^Bga>KYApl{3o8R&Drob?9al&6ICQD5JUmVn7n^6kKQU8$c8$}6E= z4X)B^pP?Yo(K`j7uM-&)9Z{`|tRW(idA_p@a}=e;Xi2qNkHh8Orgy~0FC4MxIWpi8 zx^t}^7a}!&wRrvZHV^YohC2#Kfz^BtP;n_uvX z;KFV-DVKq4BpN-G)OCdL{xZf|EcW+Xo-aZ?A)kjYVyr)qYb!Jr0gK+vQh$XttUFMW zc4gXf<&nqcN^Oejm}66o!{C77+Q~8fOh>F_j_<=BHe8hHAV=&BUClG6mH3KFgqL-{ z-!yn(@R?21hzXQ1liHqC9r>KAT_iVSzRlf#Ew(ij<)_3HsbJ}6?aq<5CaHQ z=h;5Z6u48X>^FIH&-xlMwY^_jp5+ICQMUYa^=9JK(^8wvA@S)BJ~CjFc3K4=zlmwq zx0Fko!ZV+3c(;GnTsbFR|H{z>#e8_o_RHz8ysrkTDk((Q)A+Ur2Ix_uXoBLb5cBOn zw8hiV_O#qP#mQ6q!w;^w`rISD^xb(!dwgW@gj$Lg=^cdpW_=%!5Q`yesj3Gok6`(K z1WBl(OXwM1@?99Nu99c(>#>PAxzK}7KJvX`;-nm^h#N%7&hJ7R*-(X^hN@70cb?><5u;UkfxR=TCbLlqJ=Ti>^%Hhf@kL=`KY1Q-pkPpOtk3X-FY0X6xrd>$)bYr!x|y zjIpS}^0bXo{?=rL-e+ZE^xw1NRQHD4V`Jxw^nap>a zfI^q?`TWF&kMel?uQw;=jAO~!_JObIo}W4y4}>W@0uTCTiG3$qBpRR$}tFL`iTxbHT}7ZaYV*9aMIBkGg+V) zx)1b1X(ADtFrGpVMesg;10~Myf?5PBQy30pJ+Hy#*Pd(hR}rgRYz&||06DG3xQ0ew+DU71B@_>nTA;k;4`jUDPZy_Lwf}U`Y0~TU$-bP zCm3j+*1%M(LEO`(c+Dv@a|2wyF4*aPojL^-ISqEoQPAAYK$idORZ;!F)l2{ICe`uC zRRC7ncF%ae0a!x2GlPf6GwXRSUH%u7^4bTjX_8pzxdbH3J|JW+nwJ|$>&&LWr(+NI zgTYf4DcllC2{3GK!RZofA-{L{ii2UGtg0q0hBn8h6RUWf?Wk9wKMab_Ic`2KO0nLp zTaTRT9I=RH!6$-np2q?(Vwax>E*K{DRmnB{`9cMYlm6jP*E*6!j0srm_*JR4Y~MOo zlH0c--&RgeA;MwCW6yl2$^KsfBcMu zl&~eLO4Zsx?kY*Qau#fzc<>+nxVAe!op<3%_=m=}&8M%{GKfb6Qsiz{u)eLzN}NhD zQ7)PHzh=0}PDs(o;8t;abBiH(e{uilIBh9gSt_R}>+&b}Bj z)|+`>O=oju(yr#^iV_D%4mgDywaBJ83bqH&=@Hbp_LI3uFMU;PtG#|#j^iC^4>4i2FwLwp@irw&2yLp2fh%`>N`TiXdQQ~iY z@_-G?zFHF{6sc+c!HIks{*(gw+ipmG!s3j)02-|oI0Si*#5Sc@OHYivC<^hs7D_4~ zIvJWRX4lYDby=X~7~6Gkg3!Bn^~4kHqsqU(-t7IthY{er=J-(K+bAl9Ihwk{vn1Mz zytOO}vRm6Kb;e7nj`1~4){~>H({Y!Lti>?aZYca_UY+2Jn0<0EP^dh6r`^j=>W~s7 zUu*_Cn_;9<3y5#rDIsIjb^s+?ySLTYo(n>gkDn{d=RDPRN7=trKHG$-SXJBbfm|zk z$~AO*5`ATdl6!4A<)lzLvhjMd>=`A91W$aIK4GTbzvfP%_N=ch^c+k@t%iA2?vppO z!5Y;C!Q|Y4zNC0_pXw(plS~NiRJtf;zoC>uRo-iJ#ADJ;YMVmMTM@YEU%TSky8+oL zq8R_%FY779K;&$aP@y0@TT41#Vx;*!2!spg*e~GT2;Q@HjD(Zg((*O{r0V*RB~bM- zC_jKyC-#ZB88cqv@Uo40V2o#&80{^kmP9(b41#bs5csZdjib7q4%XQV6oOoaQa>DJ zC`#Ljz5!j~z$e4dm)N+;4+`|&Z?W9cpMa3f>^VWU20Rt=tZ5r@oCCh!==wfgk^LSZ zX?J_8?Oi|Tt^>Kj-1TGJfdPoK$!&r1s||A?{hE8Y*LPSg^0);+Cm7EQo_EGM*4B=h zf2SxhV?e1CEC>w0m{Z#X3p^7P@*n}WBd}QGSs3lU4^Jb}KQPFXqyfjhG$OtE%DqyX zW@ltV5R(3(@tz%!fC51URs>(qs_`&L;MZ6EPR=PKST1?L%g30wW#0@NKh%ITHxeg# z+;s2gpCc>{r1g@+bhVk%s2w^Z!E5XeSV&mTby5y#;#hirNv=${j_5GTzEa9mYgDnb z|FeZ4l(LC+`35v@vvL^!s7%k3e!**^touednrlFxsK64x@;sh z32IQbzC_j8FW((VENk#yiY@fHn@_~AYww>YXbake#MpiF6)9$k?ugJJu}~v={*=fQ zXK7m2BsmJ4Sq}N<`}zo-O;Kk4@K0R6Gp*d7=V3p(hbMphh0J?A3&Tx(?7^#vIEq5 zcg*g{o}d8~jd0ra`V_|_oeM?#+l6U0=28tFL#?&@ce(z;I;S&cPP$ zq(i<2 z%h}8{U0_^Yfv4>CIpMgXh{OHux%<<59$g2YNyw{IZUCjZ0Li$k76(ehn=RT|66At( z=W)Pdr8I8j90m9jsAAqHL)zZd@?y4=Y_|H}LsxQWsaXMTWT=$RDWD^~?>9^a)tm-; zN!GaY<;P(>37nAtdYXam>V2c3u)JPp|IlHNL;FE30iETt5#UZT5F+Lzz|O+@K7GkK z%Jk_5P^BYIx@nL`d38krH1z%`u$))ptRKchx%`49>!wdZDhAofZ@f2HXuanGq+RuwPbXg1xd(9-)Ailb=tY zF45*aft8%+gM_(6NNWMN{#*GZwK>q0zfDDGaR0Z(evLaJt|9_Kaz-2>XqTOJ3O5%@ zus*?pXXjI8(gL*#kflnqqzNFoh9uqB2B#eaw&bHnDSc3Z|7X>mutN$@0({pct%Q?F zU&cx9P|$_?9mh=@MVcK;X!Y?|_imoKyr@n2q5GGd*ixOtLm9=JHVHCDC0X zV2wloVUh`?G1@aYRy7DaakFr@&yL#U}`J87Eff|7+`Ebf#5uq$Ay)%zp1?2ci# z^(%WoP`(Q&9>h=l{!a~~ljyD=z|Aq%8<Wv3rxhE)4`lmq2vBY?0){Woo=Ue9f2K6CDtTxR-@V$TL56v1bRB!=&h)TNSo9k zu0}gib?14nR+`4FB0Up~=ayALqT|ao3b+KS#Y#-sIuB6o`F(jd)9NS>7((P*3FUz; z6kz#sfiN~A-OUg}1_zXoRyFoXL>1vXufQ<@)SZA^wRH-mxSE$_e)DDHyz~b`h%jFe zcgfY;jdA5IN#ygoM zs`3j%M&A~ni}&3ali4EN=it#}MDY44M-ANohBxwBZ>lQ)tHeJm@6)VD^}nxA)68T4 zY2E)P*A#O!%xCS-;(fvVpNm)cbTnYx`L}pgMvaS<*qWp%P9uT$@5}wSePpF{1q;2A zs|kTo0Xy-`Z?#3YJ2eIz66_u40U$GAzMcBWQ5}GLlUzzNe>BG&kZOh;7N1}70XjwIG9_?!1A8%Kb zTpY;HY<>^abnX@By9L7!H`y9y02y2q*iExG?1FE|O% z<8|hH^S_f7Ec(au9pGyJf_5}9d*67vn}WW5zpb|Skhm0KSskx`S!5I(y;NGTd*LIv zawnyHaQhW;DP{8o{*YvR_&sL?Nc=Vq90s2dH)+;K+7B$bBfe4>KvBg&bofEnsVTIS z{qB|3R`VFJ_VobgUD(%lxWAgxX5Q)lcBF0VMP+z{|G2yqEyb2;G5@t?Ux7y94#0>M zmDw9NoIDdYBhlP_3v_Cx6&cdOf5hFIK#5R#{p_4nekPBx`L5YECr`1xXut=CuUOgp zSWUM@N6pVtbW94qYwz@BR|X6p5??4s-%i~%e1(4!3bJFBjO0o;w4quxaT8CLPyZ(t zMV0z2ikZ`Za}1f_a4F@)rOPrOXui(yzI?6UsEj8^M<>f|VpxTCzVC4+bQe6|BlO3r zqx!%l3|CJDD@{55M9l7TZ*xSqcw>hx9e8pBocarm$pdG`94XVSQb`QUds-LZkW6^% zup$gjL2QlrTI!3x(}%7Mq}FsV&7cVi52hDQP!P%Ko0>AV8-^RmEE7I38E`wjIm zdSdU3?Wri7&oR5gWi66)KTs}~ZhZR;R1ZlH>#xFj$yZx-7_Zl#CXP4aAf>AyeEMk! zBT4fPZF=BOFLB!dUWNAZbwi*j}oJ&hOrXfCx(MmLhxHgs7>^?Vd~_=%Ye_3qK+1xrYNBEPux>x zj_wKF)J5Ve)eE1FD-2w<=%@Qo=$IS~MTJ|b8rhx>bRDL$Prjwj z!ThAUItS!P94V4ffcK2kw&@96>U-b};m7T^Z543zGRAd|PvnRLRtL~y2pSN(Wa2=U z15tcKe8>RC^X)6!*+;9d2o4G&QFb^f`Ok96AT`L(+|Tor06I3|Za$$5<8gk~TD$mC zl%y?fg>y{%iNCt0NE@uKUQ#?Vjfq%ko_C4WayR_VjGy^xHoC-SA6wYb`7-b%I%5`~ zq!L=vNa+-2NgUZ8gWm|bt_wI*>amv8($(;hQ5C2cE0qx&vknV&bjBwK^rRq2L!)ET z2RmN+wYH>CfI6vp>H!cJJ+TjZ$JTq&DHfLtxU!EYS|tw#XIWe<4xTc^#xy zch={yPTwMK$UFbGIClLO{m=16V~|`Gq$KSC6EIFR$HlaO*vBMUF-=ut_tg=m4>NCX zyMM^!6Pxeo$)DxDni6-Q_t`Z=2}cNa=jozZ#%|D^a04MSR!!-_ z>c^Rcr|+mA`q1Cvec=16#gfABgnRsAV`G!Skccq!P=p*8a5;KkL!wLevyD|3Xp?)_ zH!UAoJ4L^^Hh*P0=x}YMor;P&j`IHn;7&W}gtpC0d}#i9#y=DzSTzmm;pYFT8_>U$~nuo z02wD7%P9YI&Qc4|#SdYelpRMoAp=p4ki9@TXUPNDO#q_6ZRQVB00+;!E?xL@&Jy7H zn9Es5Dd#K`KuB|viQDkcIm_FC`qfI3{c}_loOJPw_~oTUIV!3KpgNCU*>#W{$aDte z^V^%+gJ9v%{{{yZ_f7a2DMv+5f>4BmTlwCfqoPcJ3cY>W`_EBPa4M(o*H=O)rF$_z fr}+PH2#4&qH6Qrsac$>Qfj_!%BUp{*)f@i@+VzeG literal 0 HcmV?d00001 diff --git a/static/diagrams/threaded-dag.drawio b/static/diagrams/threaded-dag.drawio new file mode 100644 index 00000000..e481b875 --- /dev/null +++ b/static/diagrams/threaded-dag.drawio @@ -0,0 +1 @@ +7VpbU+IwFP41PMqkTS/wKDfdGXXcdWdXn5wsDTTaNkwIAvvrN6UJbYlCF1gbXcYZTU5P0ub0u6S1DdiNFxcMTcJrGuCoYYNg0YC9hm3bwLPFnzSyzCIW8NwsMmYkkLE8cEd+Y5UoozMS4GkpkVMacTIpB4c0SfCQl2KIMTovp41oVD7rBI2xFrgbokiP/iQBD7Noy/bz+CUm41Cd2fLa2ZEYqWS5kmmIAjovhGC/AbuMUp614kUXR2n1VF2ycYM3jq4vjOGEVxnww3ukD17wHF/f3IRXj52v/dA9k7O8oGgmF3wur5YvVQnmIeH4boKGaX8u7nMDdkIeR6JniSaaTrLCj8gCi3N15JSYcbx481qtdQUEdjCNMWdLkaIGQFk0BRtH9uf5PVApYaH8KobkXR+vZ84LIxqyNn9Tp5ZWFhwIoMguZTykY5qgqJ9HO4zOkiAtyapOec4VpRMZfMKcLyXq0YzTcmnxgvB70Qay/ZC2m67s9RaFQ72l6iRivfdqgrRTGJV282Grnho3ogkfoJhEaeA7iQXpbHCD5+L3NxqjZH1j03Vvv62iTHTGhnhLPaUscMTGmO/Cpw4ThiPEyUv5Oo5+022NHB0TyGED08jhn8hxTHLAiuSwayWHr7EjbvKQYRQcjIanWTxR+QlN8IEAadpuASNWNYSApu8WQWLtgEiApuF6AWnnFnGOWbKK2MAR0Qj9wlEHDZ/Hq+V2aUTZqkBw0E1/NoF2iaMXzMkQ1QOvWrUXaujqGqG9vmna2z5p7zHJ4VQkB6xVe52Po71F6QXVALIpvWAHQj6O9FZFV63OroOrZ4L0Qsc06XVP0ntMcrgVyeHUKr3ux5He/ba99ifd9laF13YVOgNNB/pSIitDbjXdOWNoWUiYUJLwaeFst2kgP5WjZpSCp577B2/kW7C1LV80sivYGK0uh45GU1GYTYKsS7A/Z3TK9E0wFMeCTdcwS3FOlnJMznsVOe/Wain6e0ZjLWWv3bz9SXfzVdF16G5+L/dwwYa27XAPG7S25Zfd41jO4GnIHxjhDG3THjXgyReOyVy/InO9Wn1BZ0fcFK4w5IQmRjvDf/+KvSq+Dn2LuJ8ztMrOoH04sOkMbbgt/984g/7fpQsTnME17v2/fZASgJMzlOvZrshcv1ZnAK84Aw4If7ddwrvq/puKXcEQpkIeSDL+vloMyAOi54LXbbCA8wp2UgP2an1abWvQuzRBmD3XNGG2tLKchPkAcqgPQneyo10nOyxdmb+YQA///T6oFN38o9ZsO5h/Gwz7fwA= \ No newline at end of file diff --git a/static/diagrams/threaded-dag.png b/static/diagrams/threaded-dag.png new file mode 100644 index 0000000000000000000000000000000000000000..085d0f0915a41eb5279332cb6d7f83852d22e8b0 GIT binary patch literal 11621 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 R!F$t82D+v?xmxEh{tuJ*8)^Um literal 0 HcmV?d00001